Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion app/controllers/concerns/metrics_utils.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,22 @@ module MetricsUtils
def check_clickhouse
unless clickhouse_connected?
render partial: "metrics/error_card", locals: {
metrics_path: params[:action]
metrics_path: params[:action],
card_type: card_type(params[:action].to_sym)
}
end
end

def card_type(action)
blank_type = %i[episode_sparkline]

if blank_type.include?(action)
"blank"
else
"error"
end
end

def generate_date_range(date_start, date_end, interval)
start_range = date_start.to_datetime.utc.send(:"beginning_of_#{interval.downcase}")
end_range = date_end.to_datetime.utc.send(:"beginning_of_#{interval.downcase}")
Expand Down
77 changes: 40 additions & 37 deletions app/controllers/podcast_metrics_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,31 @@ class PodcastMetricsController < ApplicationController

before_action :set_podcast
before_action :check_clickhouse, except: %i[show]
before_action :set_date_range, except: %i[dropdays]
before_action :set_uniques, only: %i[show uniques]
before_action :set_dropday_range, only: %i[show dropdays]

def show
end

def episode_sparkline
@episode = Episode.find_by(guid: metrics_params[:episode_id])
@prev_episode = Episode.find_by(guid: metrics_params[:prev_episode_id])

@episode_trend = calculate_episode_trend(@episode, @prev_episode)
@sparkline_downloads =
Rollups::HourlyDownload
.where(episode_id: @episode[:guid], hour: (@episode.first_rss_published_at..))
.final
.select(:episode_id, "DATE_TRUNC('DAY', hour) AS hour", "SUM(count) AS count")
.group(:episode_id, "DATE_TRUNC('DAY', hour) AS hour")
.order(Arel.sql("DATE_TRUNC('DAY', hour) ASC"))
.load_async

render partial: "metrics/episode_sparkline", locals: {
episode: @episode,
downloads: @sparkline_downloads,
episode_trend: @episode_trend
}
end

def downloads
@downloads_within_date_range =
Rollups::HourlyDownload
Expand Down Expand Up @@ -195,46 +213,31 @@ def set_podcast
render_not_found(e)
end

def set_date_range
@date_start = metrics_params[:date_start]
@date_end = metrics_params[:date_end]
@interval = metrics_params[:interval]
@date_range = generate_date_range(@date_start, @date_end, @interval)
def metrics_params
params
.permit(:podcast_id, :episode_id, :prev_episode_id)
end

def set_uniques
@uniques_selection = uniques_params[:uniques_selection]
end
def calculate_episode_trend(episode, prev_episode)
return nil unless episode.in_default_feed? && episode.first_rss_published_at.present? && prev_episode.present?
return nil if (episode.first_rss_published_at + 1.day) > Time.now

def set_dropday_range
@dropday_range = dropdays_params[:dropday_range]
end
ep_dropday_sum = episode_dropday_query(episode)
previous_ep_dropday_sum = episode_dropday_query(prev_episode)

def metrics_params
params
.permit(:podcast_id, :date_start, :date_end, :interval)
.with_defaults(
date_start: 30.days.ago.utc_date,
date_end: Date.utc_today,
interval: "DAY"
)
end
return nil if ep_dropday_sum <= 0 || previous_ep_dropday_sum <= 0

def uniques_params
params
.permit(:uniques_selection)
.with_defaults(
uniques_selection: "last_7_rolling"
)
.merge(metrics_params)
((ep_dropday_sum.to_f / previous_ep_dropday_sum.to_f) - 1).round(2)
end

def dropdays_params
params
.permit(:dropday_range, :interval)
.with_defaults(
dropday_range: 7
)
.merge(metrics_params)
def episode_dropday_query(ep)
lowerbound = ep.first_rss_published_at.beginning_of_hour
upperbound = lowerbound + 24.hours

Rollups::HourlyDownload
.where(episode_id: ep[:guid], hour: (lowerbound...upperbound))
.final
.load_async
.sum(:count)
end
end
27 changes: 26 additions & 1 deletion app/controllers/podcasts_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,12 @@ def index
def show
authorize @podcast

@recently_published = @podcast.episodes.published.dropdate_desc.limit(3)
@recently_published_episodes = @podcast.episodes.published.dropdate_desc.limit(4)
@trend_episodes = @podcast.default_feed.episodes.published.dropdate_desc.where.not(first_rss_published_at: nil).offset(1).limit(4)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here i am pulling the episodes to be compared against in the trend calculation. offset by 1 because i don't want the most recent one.

@episode_trend_pairs = episode_trend_pairs(@recently_published_episodes, @trend_episodes)

# @recently_published is used for the prod branch
@recently_published = @recently_published_episodes[0..2]
@next_scheduled = @podcast.episodes.draft_or_scheduled.dropdate_asc.limit(3)

@metrics_jwt = prx_jwt
Expand Down Expand Up @@ -189,4 +194,24 @@ def compose_message(podcast)
def sub_escapes(text)
text.gsub(/[&<>]/, "&" => "&amp;", "<" => "&lt;", ">" => "&gt;")
end

def episode_trend_pairs(episodes, trend_episodes)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and here i realized that i didn't want to necessarily pair episode and trend episode 1:1. i still needed to check if an episode should have a trend number at all, and then count which trend_episodes have been used in pairing. in most cases this should still line up 1:1, but should catch the cases where there's an episode that is not in the default feed and needs to be skipped in trend calculation.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since these are in separate turbo requests, you could just find which episode you should compare to in that other request? It avoids any kind of N+1, not attempting to pair them all upfront. And makes the outer request quicker.

Episode.where(podcast_id: ep.podcast_id, first_rss_published_at: ..ep.first_rss_published_at).order(first_rss_published_at: :desc).first

(Not a request for change in this PR, just something to think about ... optimize for the outer/first GET request).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added to #1400

paired_trend_episodes = []

episodes.map.with_index do |ep, i|
if ep.in_default_feed?
paired_trend_episodes << trend_episodes[paired_trend_episodes.length]

{
episode: ep,
prev_episode: paired_trend_episodes.last
}
else
{
episode: ep,
prev_episode: nil
}
end
end
end
end
34 changes: 34 additions & 0 deletions app/helpers/metrics_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,40 @@ def sum_rollups(rollups)
rollups.sum(&:count)
end

def parse_trend(trend)
return if trend.blank?

if trend > 0
{
percent: "+#{(trend * 100).round(2)}%",
color: modified_trend_color(trend, "text-success"),
direction: modified_trend_direction(trend, "up")
}
elsif trend < 0
{
percent: "-#{(trend * -100).round(2)}%",
color: modified_trend_color(trend, "text-danger"),
direction: modified_trend_direction(trend, "down")
}
end
end

def modified_trend_direction(trend, direction)
if trend.abs > 0.05
"trending_#{direction}"
else
"trending_flat"
end
end

def modified_trend_color(trend, color)
if trend.abs > 0.05
color
else
"text-secondary"
end
end

def interval_options
Rollups::HourlyDownload::INTERVALS.map { |i| [I18n.t(".helpers.label.metrics.interval.#{i.downcase}"), i] }
end
Expand Down
27 changes: 0 additions & 27 deletions app/javascript/controllers/apex_downloads_controller.js

This file was deleted.

34 changes: 34 additions & 0 deletions app/javascript/controllers/apex_sparkline_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Controller } from "@hotwired/stimulus"
import { buildSparklineChart, destroyChart } from "util/apex"

export default class extends Controller {
static values = {
id: String,
downloads: Array,
}

static targets = ["chart"]

connect() {
const seriesData = this.downloadsValue.map((rollup) => {
return {
x: rollup.hour,
y: rollup.count,
}
})
const series = [
{
name: "Downloads",
data: seriesData,
},
]

const chart = buildSparklineChart(this.idValue, series, this.chartTarget)

chart.render()
}

disconnect() {
destroyChart(this.idValue)
}
}
46 changes: 46 additions & 0 deletions app/javascript/util/apex.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,48 @@ export const AREA_TYPE = {
},
}

export const SPARKLINE_TYPE = {
chart: {
height: "100%",
sparkline: {
enabled: true,
},
animations: {
enabled: true,
},
type: "area",
stacked: false,
},
options: {
xaxis: {
type: "datetime",
},
tooltip: {
enabled: false,
},
fill: {
colors: ["#F3EAFF"],
type: "gradient",
gradient: {
type: "diagonal1",
gradientToColors: ["#DBFBFB"],
opacityFrom: 1,
opacityTo: 1,
},
},
stroke: {
width: 1,
colors: ["#00000000"],
},
},
}

export function buildSparklineChart(id, series, target) {
const options = Object.assign({ series: series }, DEFAULT_OPTIONS, SPARKLINE_TYPE.options)
Object.assign(options.chart, { id: id }, SPARKLINE_TYPE.chart)
return new ApexCharts(target, options)
}

export function buildDateTimeChart(id, series, target, type, title = "") {
const options = Object.assign({ series: series }, DEFAULT_OPTIONS, DATETIME_OPTIONS, type.options)
Object.assign(options.chart, { id: id }, type.chart)
Expand Down Expand Up @@ -176,3 +218,7 @@ function addYaxisTitle(yaxis, title = "") {
function addXaxisTitle(xaxis, title = "") {
Object.assign(xaxis, { title: { text: title } })
}

export function destroyChart(chartId) {
ApexCharts.exec(chartId, "destroy")
}
14 changes: 14 additions & 0 deletions app/views/metrics/_episode_sparkline.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<%= turbo_frame_tag "episode_sparkline" do %>
<% trend = parse_trend(episode_trend) %>

<p class="d-flex align-items-center <%= trend[:color] if trend.present? %>">
<span class="z-1"><%= trend[:percent] if trend.present? %></span>
<span class="material-icons ms-1 z-1" aria-hidden="true"><%= trend[:direction] if trend.present? %></span>
</p>
<div class="position-absolute top-50 start-50 translate-middle w-100 h-100 z-0"
data-controller="apex-sparkline"
data-apex-sparkline-id-value="<%= episode.guid %>"
data-apex-sparkline-downloads-value="<%= downloads.to_json %>">
<div data-apex-sparkline-target="chart"></div>
</div>
<% end %>
56 changes: 7 additions & 49 deletions app/views/metrics/_episodes_card.html.erb
Original file line number Diff line number Diff line change
@@ -1,50 +1,8 @@
<% chart_id = SecureRandom.uuid %>

<%= turbo_frame_tag "episodes" do %>
<%= form_with(url: url, method: :get, id: form_id, data: {controller: "click", click_debounce_value: 200}) do |form| %>
<div class="row d-flex flex-wrap gx-4 h-100">
<div class="col metrics-chart"
data-controller="apex-episodes"
data-apex-episodes-id-value="<%= chart_id %>"
data-apex-episodes-selected-episodes-value="<%= episode_rollups.to_json %>"
data-apex-episodes-date-range-value="<%= date_range.to_json %>"
data-apex-episodes-interval-value="<%= interval %>">
<div data-apex-episodes-target="chart"></div>
</div>
<div class="col align-items-center metrics-table" data-controller="apex-toggles">
<table class="table table-sm table-striped shadow rounded">
<thead class="table-primary">
<tr>
<td><%= t(".table_header.episodes") %></td>
<td><%= t(".table_header.published_at") %></td>
<td><%= t(".table_header.downloads") %></td>
<td><%= t(".table_header.all_time") %></td>
</tr>
</thead>
<tbody>
<% episode_rollups.each_with_index do |er, i| %>
<% sum = sum_rollups(er[:rollups]) %>
<% total = sum_rollups(er[:totals]) %>
<% title = er[:episode][:title] %>
<tr class="align-items-center">
<td class="d-flex align-items-center">
<input id="episode<%= title %>" type="checkbox" class="form-check-input mt-0 me-2" checked="true" data-action="click->apex-toggles#toggleSeries" data-apex-toggles-series-param="<%= title %>" data-apex-toggles-id-param="<%= chart_id %>">
<span class="material-icons me-2" aria-label="hidden" style="color: <%= er[:color] %>">circle</span>
<span class="text-nowrap overflow-hidden"><%= link_to title, episode_metrics_path(episode_id: er[:episode][:guid], date_start: date_start, date_end: date_end, interval: interval), data: {turbo_frame: "_top"} %></span>
</td>
<td><%= er[:episode][:published_at].strftime("%Y-%m-%d") %></td>
<td><% if sum %><%= sum %><% else %>&mdash;<% end %></td>
<td><% if total %><%= total %><% else %>&mdash;<% end %></td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>

<%= form.date_field :date_start, class: "d-none", data: {apex_nav_target: "startDate", action: "change->click#submit"}, value: date_start %>
<%= form.date_field :date_end, class: "d-none", data: {apex_nav_target: "endDate", action: "change->click#submit"}, value: date_end %>
<%= form.select :interval, interval_options, {selected: interval}, class: "d-none", data: {apex_nav_target: "interval", action: "change->click#submit", path: "episodes"} %>
<%= form.submit class: "d-none", data: {click_target: "submit"} %>
<div class="episode-card-info inside-card flex-fill px-3 py-2 position-relative">
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

repurposing this partial for the published episode cards.

<p class="episode-card-date z-1"><%= local_date(episode.published_at, format: :short) %></p>
<div class="episode-card-inner inside-card">
<h2 class="h5 episode-card-title m-0 z-1"><%= link_to episode.title, episode_path(episode) %></h2>
</div>
<%= turbo_frame_tag "episode_sparkline", src: episode_sparkline_podcast_metrics_path(podcast_id: podcast.id, episode_id: episode.guid, prev_episode_id: prev_episode.guid), loading: "lazy" do %>
<% end %>
<% end %>
</div>
Loading