Skip to content

Two fixes related to main graph and revenue metrics#6331

Open
RobertJoonas wants to merge 8 commits into
masterfrom
fix-graph-revenue-metric-500
Open

Two fixes related to main graph and revenue metrics#6331
RobertJoonas wants to merge 8 commits into
masterfrom
fix-graph-revenue-metric-500

Conversation

@RobertJoonas
Copy link
Copy Markdown
Contributor

Changes

  • Code refactor: organize QueryBuilder.do_build a bit better, using a pipeline with steps rather than a long function.
  • Fix 1: querying revenue metrics while including imported data resulted in invalid SQL due to the imported pagination optimization
  • Fix 2: optimistic graph fetch 500 -- stored graph metric is a revenue metric -> goal filter gets removed -> optimistic attempt to fetch the graph -> revenue metrics dropped -> empty metrics -> invalid SQL.

Tests

  • Automated tests have been added

Changelog

  • No update needed. Both bugs surfaced with main graph v2 which still hasn't made it to a CE release

Documentation

  • This change does not need a documentation update

Dark mode

  • This PR does not change the UI

This fixes a 500 error where we're trying to optimistically fetch the
main graph with a stored revenue metric, while the goal filter has been
removed.

Rather than dropping revenue metrics in QueryOptimizer (which does not
validate anything), we will do it in QueryBuilder instead. Query building
will fail when dropping revenue metrics leaves query.metrics empty.
@RobertJoonas RobertJoonas force-pushed the fix-graph-revenue-metric-500 branch from e9a5f22 to 2df67ea Compare May 8, 2026 14:36
@RobertJoonas RobertJoonas requested a review from a team May 11, 2026 07:23
Comment thread lib/plausible/stats/query_builder.ex Outdated
{%{relative_date: relative_date}, query_fields} =
Map.split(parsed_query_params, [:relative_date, :skip_goal_existence_check])

struct!(%Query{}, Map.to_list(query_fields))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

much nicer like this, you could start the pipeline with %Query{} too. There's also no need for Map.to_list/1, the signature is struct!(module() | struct(), Enumerable.t()) :: struct()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If the intention is to rewrite ParsedQueryParams into Query struct, whenever a key exists on both ends we could do something like:

Map.take(parsed_query_params,  Map.keys(Query.__struct__())

and then refer to parsed_query_params.relative_date

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

And perhaps move this to ParsedQueryParams.to_query()?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

We're Map.splitting a struct so query_fields is a struct too. Removing Map.to_list results in (Protocol.UndefinedError) protocol Enumerable not implemented for Plausible.Stats.ParsedQueryParams (a struct).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

For some reason didn't see the other 2 comments before. But your suggested approach sounds good. I've extracted ParsedQueryParams.to_query!/1 that finds the common query fields and builds the %Query{} struct.

Comment thread lib/plausible/stats/query_builder.ex Outdated

defp do_build(parsed_query_params, site, debug_metadata) do
now = parsed_query_params.now || DateTime.utc_now(:second)
{%{relative_date: relative_date}, query_fields} =
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Does it effectively mean we discard skip_goal_existence_check? Unclear what's the reasoning behind it. Not sure why do we split at all, if we only care about relative_date. Can you elaborate?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

We split to get a map with direct query fields. Not everything in ParsedQueryParams will make it into the actual query struct. Some fields are just required to build a query.

relative_date is used for constructing a utc_time_range which is why this function needs it. As for skip_goal_existence_check that's used elsewhere in the QueryBuilder.build pipeline. This function simply ignores it indeed.

Comment thread lib/plausible/stats/query_builder.ex Outdated
Comment on lines +255 to +256
if query.include.drop_unavailable_revenue_metrics and
map_size(query.revenue_currencies) == 0 do
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

maybe pattern match against that in function head/guard?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@aerosol aerosol requested review from Copilot and removed request for Copilot May 11, 2026 13:32
skip_goal_existence_check: false

def to_query!(%__MODULE__{} = parsed_query_params) do
query_fields = (Query.__struct__() |> Map.keys()) -- [:__struct__]
Copy link
Copy Markdown
Member

@aerosol aerosol May 12, 2026

Choose a reason for hiding this comment

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

%Query{} |> Map.from_struct() |> Map.keys()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants