Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(unique-count): Add prorated unique count method #1694

Merged
merged 1 commit into from
Feb 15, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
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
Loading