Skip to content

Commit

Permalink
feat(unique-count): Add prorated unique count method
Browse files Browse the repository at this point in the history
  • Loading branch information
rsempe committed Feb 15, 2024
1 parent 9fd29cd commit 50eac4b
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 1 deletion.
46 changes: 45 additions & 1 deletion app/services/events/stores/postgres/unique_count_query.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,35 @@ def query
SQL
end

def prorated_query
<<-SQL
#{events_cte_sql},
event_values AS (
SELECT
property,
operation_type,
timestamp
FROM (
SELECT
timestamp,
property,
operation_type,
#{operation_value_sql} AS adjusted_value
FROM events_data
ORDER BY timestamp ASC
) adjusted_event_values
WHERE adjusted_value != 0 -- adjusted_value = 0 does not impact the total
GROUP BY property, operation_type, timestamp
)
SELECT SUM(period_ratio) as aggregation
FROM (
SELECT (#{period_ratio_sql}) AS period_ratio
FROM event_values
) cumulated_ratios
SQL
end

# NOTE: Not used in production, only for debug purpose to check the computed values before aggregation
# Returns an array of event's timestamp, property, operation type and operation value
# Example:
Expand Down Expand Up @@ -62,7 +91,7 @@ def breakdown_query

attr_reader :store

delegate :events, :sanitized_property_name, to: :store
delegate :events, :charges_duration, :sanitized_property_name, to: :store

def events_cte_sql
# NOTE: Common table expression returning event's timestamp, property name and operation type.
Expand Down Expand Up @@ -99,6 +128,21 @@ def operation_value_sql
END
SQL
end

def period_ratio_sql
<<-SQL
CASE WHEN operation_type = 'add'
THEN
-- NOTE: duration in seconds between current event and next one - using end of period as final boundaries
EXTRACT(EPOCH FROM LEAD(timestamp, 1, :to_datetime) OVER (PARTITION BY property ORDER BY timestamp) - timestamp)
/
-- NOTE: full duration of the period
#{charges_duration.days.to_i}
ELSE
0 -- NOTE: duration was null so usage is null
END
SQL
end
end
end
end
Expand Down
15 changes: 15 additions & 0 deletions app/services/events/stores/postgres_store.rb
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,21 @@ def unique_count_breakdown
).rows
end

def prorated_unique_count
query = Events::Stores::Postgres::UniqueCountQuery.new(store: self)
sql = ActiveRecord::Base.sanitize_sql_for_conditions(
[
query.prorated_query,
{
to_datetime: to_datetime.ceil,
},
],
)
result = ActiveRecord::Base.connection.select_one(sql)

result['aggregation']
end

def max
events.maximum("(#{sanitized_property_name})::numeric")
end
Expand Down
32 changes: 32 additions & 0 deletions spec/services/events/stores/postgres_store_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,38 @@
end
end

describe '#prorated_unique_count' do
it 'returns the number of unique active event properties' do
create(
:event,
organization_id: organization.id,
external_subscription_id: subscription.external_id,
external_customer_id: customer.external_id,
code:,
timestamp: boundaries[:from_datetime] + 1.day,
properties: { billable_metric.field_name => 2 },
)

create(
:event,
organization_id: organization.id,
external_subscription_id: subscription.external_id,
external_customer_id: customer.external_id,
code:,
timestamp: boundaries[:from_datetime] + 2.days,
properties: {
billable_metric.field_name => 2,
operation_type: 'remove',
},
)

event_store.aggregation_property = billable_metric.field_name

# NOTE: Events calculation: 16/31 + 1/31 + 14/31 + 13/31 + 12/31
expect(event_store.prorated_unique_count.round(3)).to eq(1.806)
end
end

describe '#events_values' do
it 'returns the value attached to each event' do
event_store.aggregation_property = billable_metric.field_name
Expand Down

0 comments on commit 50eac4b

Please sign in to comment.