Skip to content

Commit fbfa5e7

Browse files
committed
add download sparks
1 parent 6b4f268 commit fbfa5e7

19 files changed

Lines changed: 984 additions & 2 deletions
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# frozen_string_literal: true
2+
3+
module Priv::Analytics::Gauges
4+
class DownloadsController < Priv::Analytics::BaseController
5+
use_clickhouse
6+
7+
CACHE_TTL = 10.minutes
8+
CACHE_RACE_TTL = 1.minute
9+
10+
typed_query {
11+
param :product, type: :uuid, optional: true, as: :product_id
12+
param :package, type: :uuid, optional: true, as: :package_id
13+
param :release, type: :uuid, optional: true, as: :release_id
14+
}
15+
def show
16+
authorize! with: Accounts::AnalyticsPolicy
17+
18+
gauge = Analytics::Gauge.new(
19+
:downloads,
20+
**download_query,
21+
)
22+
23+
unless gauge.valid?
24+
render_bad_request *gauge.errors.as_jsonapi(
25+
title: 'Bad request',
26+
source: :parameter,
27+
sources: {
28+
parameters: {
29+
product_id: 'product',
30+
package_id: 'package',
31+
release_id: 'release',
32+
},
33+
},
34+
)
35+
36+
return
37+
end
38+
39+
data = Rails.cache.fetch gauge.cache_key, expires_in: CACHE_TTL, race_condition_ttl: CACHE_RACE_TTL do
40+
gauge.as_json
41+
end
42+
43+
render json: { data: }
44+
end
45+
end
46+
end
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# frozen_string_literal: true
2+
3+
module Priv::Analytics::Sparks
4+
class DownloadsController < Priv::Analytics::BaseController
5+
use_clickhouse
6+
7+
CACHE_TTL = 10.minutes
8+
CACHE_RACE_TTL = 1.minute
9+
10+
typed_query {
11+
param :product, type: :uuid, optional: true, as: :product_id
12+
param :package, type: :uuid, optional: true, as: :package_id
13+
param :release, type: :uuid, optional: true, as: :release_id
14+
param :date, type: :hash, optional: true, collapse: { format: :child_parent } do
15+
param :start, type: :date, coerce: true
16+
param :end, type: :date, coerce: true
17+
end
18+
}
19+
def show
20+
authorize! with: Accounts::AnalyticsPolicy
21+
22+
series = Analytics::Series.new(
23+
:downloads,
24+
**download_query,
25+
)
26+
27+
unless series.valid?
28+
render_bad_request *series.errors.as_jsonapi(
29+
title: 'Bad request',
30+
source: :parameter,
31+
sources: {
32+
parameters: {
33+
product_id: 'product',
34+
package_id: 'package',
35+
release_id: 'release',
36+
start_date: 'date[start]',
37+
end_date: 'date[end]',
38+
},
39+
},
40+
)
41+
42+
return
43+
end
44+
45+
data = Rails.cache.fetch series.cache_key, expires_in: CACHE_TTL, race_condition_ttl: CACHE_RACE_TTL do
46+
series.as_json
47+
end
48+
49+
render json: { data: }
50+
end
51+
end
52+
end

app/models/analytics/gauge.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ class Gauge
1111

1212
COUNTERS = {
1313
alus: ActiveLicensedUsers,
14+
downloads: Downloads,
1415
licenses: Licenses,
1516
machines: Machines,
1617
users: Users,
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# frozen_string_literal: true
2+
3+
module Analytics
4+
class Gauge
5+
class Downloads
6+
def initialize(account:, environment:, product_id: nil, package_id: nil, release_id: nil)
7+
@account = account
8+
@environment = environment
9+
@product_id = product_id
10+
@package_id = package_id
11+
@release_id = release_id
12+
end
13+
14+
def metrics = %w[downloads]
15+
def count
16+
event_type_ids = EventType.by_pattern('artifact.downloaded')
17+
.collect(&:id)
18+
return {} if
19+
event_type_ids.empty?
20+
21+
scope = EventLog::Clickhouse.for_account(account)
22+
.for_environment(environment)
23+
.where(
24+
event_type_id: event_type_ids,
25+
created_date: Date.current,
26+
)
27+
.where(
28+
'metadata.product IS NOT NULL',
29+
)
30+
31+
unless product_id.nil?
32+
scope = scope.where(
33+
Arel.sql('metadata.product.:String') => product_id,
34+
)
35+
end
36+
37+
unless package_id.nil?
38+
scope = scope.where(
39+
Arel.sql('metadata.package.:String') => package_id,
40+
)
41+
end
42+
43+
unless release_id.nil?
44+
scope = scope.where(
45+
Arel.sql('metadata.release.:String') => release_id,
46+
)
47+
end
48+
49+
count = scope.count
50+
51+
{ 'downloads' => count }.reject { _2.zero? }
52+
end
53+
54+
private
55+
56+
attr_reader :account, :environment, :product_id, :package_id, :release_id
57+
end
58+
end
59+
end

app/models/analytics/series.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ class Series
77
Bucket = Data.define(:metric, :date, :count)
88

99
COUNTERS = {
10+
downloads: Sparks::Downloads,
1011
events: Events,
1112
requests: Requests,
1213
sparks: Sparks,
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# frozen_string_literal: true
2+
3+
module Analytics
4+
class Series
5+
class Sparks
6+
class Downloads
7+
def initialize(account:, environment:, product_id: nil, package_id: nil, release_id: nil, realtime: true, **)
8+
@account = account
9+
@environment = environment
10+
@product_id = product_id
11+
@package_id = package_id
12+
@release_id = release_id
13+
@realtime = realtime
14+
end
15+
16+
def metrics = %w[downloads]
17+
def count(start_date:, end_date:)
18+
scope = ReleaseDownloadSpark.for_account(account)
19+
.for_environment(environment)
20+
.where(
21+
created_date: start_date..end_date,
22+
)
23+
24+
unless product_id.nil?
25+
scope = scope.where(product_id:)
26+
end
27+
28+
unless package_id.nil?
29+
scope = scope.where(package_id:)
30+
end
31+
32+
unless release_id.nil?
33+
scope = scope.where(release_id:)
34+
end
35+
36+
rows = scope.group(:created_date)
37+
.pluck(
38+
:created_date,
39+
Arel.sql('sum(count)'),
40+
)
41+
42+
counts = rows.each_with_object({}) do |(date, count), hash|
43+
hash[['downloads', date]] = count
44+
end
45+
46+
# defer to gauge for a realtime count since sparks are nightly
47+
if realtime? && end_date.today?
48+
gauge = Analytics::Gauge.new(:downloads, account:, environment:, product_id:, package_id:, release_id:)
49+
50+
gauge.measurements.each do |measurement|
51+
counts[[measurement.metric, end_date]] = measurement.count
52+
end
53+
end
54+
55+
counts
56+
end
57+
58+
private
59+
60+
attr_reader :account,
61+
:environment,
62+
:product_id,
63+
:package_id,
64+
:release_id,
65+
:realtime
66+
67+
def realtime? = !!realtime
68+
end
69+
end
70+
end
71+
end
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# frozen_string_literal: true
2+
3+
class ReleaseDownloadSpark < ClickhouseRecord
4+
include Accountable, Environmental
5+
6+
has_environment
7+
has_account
8+
end

app/services/broadcast_event_service.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ def call
5151
{ product: resource.product_id, package: resource.package_id, prev: meta[:current], next: meta[:next] }
5252
when /^artifact\.downloaded$/,
5353
/^release\.downloaded$/
54-
{ product: resource.product_id, package: resource.package_id, version: resource.version }
54+
{ product: resource.product_id, package: resource.package_id, release: resource.release_id, version: resource.version }
5555
when /^license\.validation\./
5656
{ code: meta[:code] }
5757
when /\.updated$/
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# frozen_string_literal: true
2+
3+
class RecordReleaseDownloadSparkWorker < BaseWorker
4+
sidekiq_options queue: :cron,
5+
cronitor_enabled: true
6+
7+
def perform(account_id)
8+
event_type_ids = EventType.by_pattern('artifact.downloaded')
9+
.collect(&:id)
10+
return if
11+
event_type_ids.empty?
12+
13+
events_cte = EventLog::Clickhouse.where(account_id:, created_date: Date.yesterday, event_type_id: event_type_ids)
14+
.where('metadata.product IS NOT NULL')
15+
.select(
16+
:account_id,
17+
:environment_id,
18+
:created_date,
19+
:created_at,
20+
'metadata.product.:String AS product_id',
21+
'metadata.package.:String AS package_id',
22+
'metadata.release.:String AS release_id',
23+
)
24+
25+
agg_cte = EventLog::Clickhouse.from('release_download_events')
26+
.select(
27+
:account_id,
28+
:environment_id,
29+
:created_date,
30+
'max(created_at) AS created_at',
31+
:product_id,
32+
:package_id,
33+
:release_id,
34+
'count() AS count',
35+
)
36+
.group(
37+
:account_id,
38+
:environment_id,
39+
:created_date,
40+
:product_id,
41+
:package_id,
42+
:release_id,
43+
)
44+
45+
ReleaseDownloadSpark.connection.execute(<<~SQL.squish)
46+
WITH
47+
release_download_events AS (#{events_cte.to_sql}),
48+
release_download_agg AS (#{agg_cte.to_sql})
49+
INSERT INTO release_download_sparks (
50+
account_id,
51+
environment_id,
52+
created_date,
53+
created_at,
54+
product_id,
55+
package_id,
56+
release_id,
57+
count
58+
)
59+
SELECT
60+
account_id,
61+
environment_id,
62+
created_date,
63+
created_at,
64+
product_id,
65+
package_id,
66+
release_id,
67+
count
68+
FROM
69+
release_download_agg
70+
SQL
71+
end
72+
end
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# frozen_string_literal: true
2+
3+
class RecordReleaseDownloadSparksWorker < BaseWorker
4+
sidekiq_options queue: :cron,
5+
cronitor_enabled: true
6+
7+
def perform
8+
Account.unordered.paid.find_each do |account|
9+
jitter = rand(0..30.minutes) # prevent a thundering herd effect
10+
11+
RecordReleaseDownloadSparkWorker.perform_in(jitter, account.id)
12+
end
13+
end
14+
end

0 commit comments

Comments
 (0)