Skip to content
Closed
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
48 changes: 35 additions & 13 deletions products/ai_observability/backend/api/personal_spend.py
Original file line number Diff line number Diff line change
Expand Up @@ -531,15 +531,25 @@ def _truncate(rows: list[dict[str, Any]], limit: int) -> dict[str, Any]:
def _fetch_summary(
team: Team, email: str, from_dt: datetime.datetime, to_dt: datetime.datetime, product: str
) -> dict[str, Any]:
# `{product_filter}` is read into `is_scoped` once in the inner subquery rather than being
# inlined twice (in `countIf` and `sumIf`) against the outer aggregate. Duplicating a
# property-read expression like that across sibling aggregate clauses has caused ClickHouse
# to lose track of the projected column ("Not found column in block") once optimizations like
# PREWHERE pushdown treat the two textually-identical occurrences differently.
query = parse_select(
"""
SELECT
countIf({product_filter}) AS scoped_event_count,
round(sumIf(toFloat(properties.$ai_total_cost_usd), {product_filter}), 6) AS scoped_cost_usd,
countIf(is_scoped) AS scoped_event_count,
round(sumIf(cost_usd, is_scoped), 6) AS scoped_cost_usd,
count() AS event_count,
round(sum(toFloat(properties.$ai_total_cost_usd)), 6) AS total_cost_usd
FROM events
WHERE {event_in} AND {email_filter} AND {timestamp_filter}
round(sum(cost_usd), 6) AS total_cost_usd
FROM (
SELECT
toFloat(properties.$ai_total_cost_usd) AS cost_usd,
{product_filter} AS is_scoped
FROM events
WHERE {event_in} AND {email_filter} AND {timestamp_filter}
)
"""
)
result = execute_hogql_query(
Expand Down Expand Up @@ -615,18 +625,30 @@ def _fetch_by_tool(
# generation (e.g. "Bash,Read"). Split it so multi-tool generations contribute to
# each individual tool row. nullIf restores the no-tool case (where the property
# is NULL and coalesce produces an empty string) to a NULL bucket.
#
# The arrayJoin runs in an inner subquery, projecting the raw numeric columns
# alongside it, and the aggregates run outside. ClickHouse evaluates arrayJoin
# before aggregation, and mixing it into the same SELECT as an aggregate over a
# materialized property column has caused the materialized projection to go
# missing from the post-array-join block ("Not found column in block").
query = parse_select(
"""
SELECT
nullIf(arrayJoin(splitByChar(',', coalesce(properties.$ai_tools_called, ''))), '') AS tool,
tool,
count() AS generation_count,
round(sum(toFloat(properties.$ai_total_cost_usd)), 6) AS cost_usd,
round(avg(toFloat(properties.$ai_input_tokens)), 0) AS avg_input_tokens
FROM events
WHERE equals(event, '$ai_generation')
AND {product_filter}
AND {email_filter}
AND {timestamp_filter}
round(sum(cost_usd), 6) AS cost_usd,
round(avg(input_tokens), 0) AS avg_input_tokens
FROM (
SELECT
nullIf(arrayJoin(splitByChar(',', coalesce(properties.$ai_tools_called, ''))), '') AS tool,
toFloat(properties.$ai_total_cost_usd) AS cost_usd,
toFloat(properties.$ai_input_tokens) AS input_tokens
FROM events
WHERE equals(event, '$ai_generation')
AND {product_filter}
AND {email_filter}
AND {timestamp_filter}
)
GROUP BY tool
ORDER BY cost_usd DESC
LIMIT {limit}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,32 @@
# name: TestPersonalSpendQueries.test_empty_result_when_no_events
'''
/* user_id:0 request:_snapshot_@me_spend_ */
SELECT countIf(ifNull(equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, 'ai_product'), ''), 'null'), '^"|"$', ''), 'posthog_code'), 0)) AS scoped_event_count,
round(sumIf(accurateCastOrNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$ai_total_cost_usd'), ''), 'null'), '^"|"$', ''), 'Float64'), ifNull(equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, 'ai_product'), ''), 'null'), '^"|"$', ''), 'posthog_code'), 0)), 6) AS scoped_cost_usd,
SELECT countIf(is_scoped) AS scoped_event_count,
round(sumIf(cost_usd, is_scoped), 6) AS scoped_cost_usd,
count() AS event_count,
round(sum(accurateCastOrNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$ai_total_cost_usd'), ''), 'null'), '^"|"$', ''), 'Float64')), 6) AS total_cost_usd
FROM events
LEFT OUTER JOIN
(SELECT argMax(person_distinct_id_overrides.person_id, person_distinct_id_overrides.version) AS person_id,
person_distinct_id_overrides.distinct_id AS distinct_id
FROM person_distinct_id_overrides
WHERE equals(person_distinct_id_overrides.team_id, 99999)
GROUP BY person_distinct_id_overrides.distinct_id
HAVING equals(argMax(person_distinct_id_overrides.is_deleted, person_distinct_id_overrides.version), 0) SETTINGS optimize_aggregation_in_order=1) AS events__override ON equals(events.distinct_id, events__override.distinct_id)
LEFT JOIN
(SELECT person.id AS id,
replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(person.properties, 'email'), ''), 'null'), '^"|"$', '') AS properties___email
FROM person
WHERE and(equals(person.team_id, 99999), in(tuple(person.id, person.version),
(SELECT person.id AS id, max(person.version) AS version
FROM person
WHERE equals(person.team_id, 99999)
GROUP BY person.id
HAVING and(equals(argMax(person.is_deleted, person.version), 0), less(argMax(toTimeZone(person.created_at, 'UTC'), person.version), plus(now64(6, 'UTC'), toIntervalDay(1))))))) SETTINGS optimize_aggregation_in_order=1) AS events__person ON equals(if(not(empty(events__override.distinct_id)), events__override.person_id, events.person_id), events__person.id)
WHERE and(equals(events.team_id, 99999), in(events.event, tuple('$ai_generation', '$ai_embedding')), ifNull(equals(events__person.properties___email, 'user1@posthog.com'), 0), and(greaterOrEquals(events.timestamp, toDateTime64('explicit_redacted_timestamp', 6, 'UTC')), less(events.timestamp, toDateTime64('today', 6, 'UTC'))))
round(sum(cost_usd), 6) AS total_cost_usd
FROM
(SELECT accurateCastOrNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$ai_total_cost_usd'), ''), 'null'), '^"|"$', ''), 'Float64') AS cost_usd,
ifNull(equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, 'ai_product'), ''), 'null'), '^"|"$', ''), 'posthog_code'), 0) AS is_scoped
FROM events
LEFT OUTER JOIN
(SELECT argMax(person_distinct_id_overrides.person_id, person_distinct_id_overrides.version) AS person_id,
person_distinct_id_overrides.distinct_id AS distinct_id
FROM person_distinct_id_overrides
WHERE equals(person_distinct_id_overrides.team_id, 99999)
GROUP BY person_distinct_id_overrides.distinct_id
HAVING equals(argMax(person_distinct_id_overrides.is_deleted, person_distinct_id_overrides.version), 0) SETTINGS optimize_aggregation_in_order=1) AS events__override ON equals(events.distinct_id, events__override.distinct_id)
LEFT JOIN
(SELECT person.id AS id,
replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(person.properties, 'email'), ''), 'null'), '^"|"$', '') AS properties___email
FROM person
WHERE and(equals(person.team_id, 99999), in(tuple(person.id, person.version),
(SELECT person.id AS id, max(person.version) AS version
FROM person
WHERE equals(person.team_id, 99999)
GROUP BY person.id
HAVING and(equals(argMax(person.is_deleted, person.version), 0), less(argMax(toTimeZone(person.created_at, 'UTC'), person.version), plus(now64(6, 'UTC'), toIntervalDay(1))))))) SETTINGS optimize_aggregation_in_order=1) AS events__person ON equals(if(not(empty(events__override.distinct_id)), events__override.person_id, events.person_id), events__person.id)
WHERE and(equals(events.team_id, 99999), in(events.event, tuple('$ai_generation', '$ai_embedding')), ifNull(equals(events__person.properties___email, 'user1@posthog.com'), 0), and(greaterOrEquals(events.timestamp, toDateTime64('explicit_redacted_timestamp', 6, 'UTC')), less(events.timestamp, toDateTime64('today', 6, 'UTC')))))
LIMIT 100 SETTINGS readonly=2,
max_execution_time=60,
allow_experimental_object_type=1,
Expand All @@ -42,29 +45,33 @@
# name: TestPersonalSpendQueries.test_empty_result_when_no_events.1
'''
/* user_id:0 request:_snapshot_@me_spend_ */
SELECT nullIf(arrayJoin(splitByChar(',', coalesce(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$ai_tools_called'), ''), 'null'), '^"|"$', ''), ''))), '') AS tool,
SELECT tool,
count() AS generation_count,
round(sum(accurateCastOrNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$ai_total_cost_usd'), ''), 'null'), '^"|"$', ''), 'Float64')), 6) AS cost_usd,
round(avg(accurateCastOrNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$ai_input_tokens'), ''), 'null'), '^"|"$', ''), 'Float64')), 0) AS avg_input_tokens
FROM events
LEFT OUTER JOIN
(SELECT argMax(person_distinct_id_overrides.person_id, person_distinct_id_overrides.version) AS person_id,
person_distinct_id_overrides.distinct_id AS distinct_id
FROM person_distinct_id_overrides
WHERE equals(person_distinct_id_overrides.team_id, 99999)
GROUP BY person_distinct_id_overrides.distinct_id
HAVING equals(argMax(person_distinct_id_overrides.is_deleted, person_distinct_id_overrides.version), 0) SETTINGS optimize_aggregation_in_order=1) AS events__override ON equals(events.distinct_id, events__override.distinct_id)
LEFT JOIN
(SELECT person.id AS id,
replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(person.properties, 'email'), ''), 'null'), '^"|"$', '') AS properties___email
FROM person
WHERE and(equals(person.team_id, 99999), in(tuple(person.id, person.version),
(SELECT person.id AS id, max(person.version) AS version
FROM person
WHERE equals(person.team_id, 99999)
GROUP BY person.id
HAVING and(equals(argMax(person.is_deleted, person.version), 0), less(argMax(toTimeZone(person.created_at, 'UTC'), person.version), plus(now64(6, 'UTC'), toIntervalDay(1))))))) SETTINGS optimize_aggregation_in_order=1) AS events__person ON equals(if(not(empty(events__override.distinct_id)), events__override.person_id, events.person_id), events__person.id)
WHERE and(equals(events.team_id, 99999), equals(events.event, '$ai_generation'), ifNull(equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, 'ai_product'), ''), 'null'), '^"|"$', ''), 'posthog_code'), 0), ifNull(equals(events__person.properties___email, 'user1@posthog.com'), 0), and(greaterOrEquals(events.timestamp, toDateTime64('explicit_redacted_timestamp', 6, 'UTC')), less(events.timestamp, toDateTime64('today', 6, 'UTC'))))
round(sum(cost_usd), 6) AS cost_usd,
round(avg(input_tokens), 0) AS avg_input_tokens
FROM
(SELECT nullIf(arrayJoin(splitByChar(',', coalesce(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$ai_tools_called'), ''), 'null'), '^"|"$', ''), ''))), '') AS tool,
accurateCastOrNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$ai_total_cost_usd'), ''), 'null'), '^"|"$', ''), 'Float64') AS cost_usd,
accurateCastOrNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$ai_input_tokens'), ''), 'null'), '^"|"$', ''), 'Float64') AS input_tokens
FROM events
LEFT OUTER JOIN
(SELECT argMax(person_distinct_id_overrides.person_id, person_distinct_id_overrides.version) AS person_id,
person_distinct_id_overrides.distinct_id AS distinct_id
FROM person_distinct_id_overrides
WHERE equals(person_distinct_id_overrides.team_id, 99999)
GROUP BY person_distinct_id_overrides.distinct_id
HAVING equals(argMax(person_distinct_id_overrides.is_deleted, person_distinct_id_overrides.version), 0) SETTINGS optimize_aggregation_in_order=1) AS events__override ON equals(events.distinct_id, events__override.distinct_id)
LEFT JOIN
(SELECT person.id AS id,
replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(person.properties, 'email'), ''), 'null'), '^"|"$', '') AS properties___email
FROM person
WHERE and(equals(person.team_id, 99999), in(tuple(person.id, person.version),
(SELECT person.id AS id, max(person.version) AS version
FROM person
WHERE equals(person.team_id, 99999)
GROUP BY person.id
HAVING and(equals(argMax(person.is_deleted, person.version), 0), less(argMax(toTimeZone(person.created_at, 'UTC'), person.version), plus(now64(6, 'UTC'), toIntervalDay(1))))))) SETTINGS optimize_aggregation_in_order=1) AS events__person ON equals(if(not(empty(events__override.distinct_id)), events__override.person_id, events.person_id), events__person.id)
WHERE and(equals(events.team_id, 99999), equals(events.event, '$ai_generation'), ifNull(equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, 'ai_product'), ''), 'null'), '^"|"$', ''), 'posthog_code'), 0), ifNull(equals(events__person.properties___email, 'user1@posthog.com'), 0), and(greaterOrEquals(events.timestamp, toDateTime64('explicit_redacted_timestamp', 6, 'UTC')), less(events.timestamp, toDateTime64('today', 6, 'UTC')))))
GROUP BY tool
ORDER BY cost_usd DESC
LIMIT 51 SETTINGS readonly=2,
Expand Down
Loading