diff --git a/lib/sanbase/available_metrics/available_metrics.ex b/lib/sanbase/available_metrics/available_metrics.ex index f2c5659c63..2b480c6280 100644 --- a/lib/sanbase/available_metrics/available_metrics.ex +++ b/lib/sanbase/available_metrics/available_metrics.ex @@ -86,6 +86,7 @@ defmodule Sanbase.AvailableMetrics do metric_to_supported_assets_map = metric_to_available_slugs_maps() access_map = Sanbase.Metric.Helper.access_map() metric_to_categories = metric_to_categories_map() + hidden_metrics = Sanbase.Metric.hidden_metrics() metrics |> Enum.map(fn metric -> @@ -99,6 +100,12 @@ defmodule Sanbase.AvailableMetrics do internal_name: m.internal_metric, status: m.status, docs: Map.get(m, :docs) || [], + # `is_deprecated` is the derived flag: the registry's own column, or a + # `hard_deprecate_after` that is set - including a date still in the + # future, which is a metric on its way out and not worth advertising. + is_deprecated: Map.get(m, :is_deprecated) == true, + hard_deprecate_after: Map.get(m, :hard_deprecate_after), + is_hidden: MapSet.member?(hidden_metrics, m.metric), available_assets: Map.get(metric_to_supported_assets_map, m.metric) || [], default_aggregation: m.default_aggregation, frequency: m.min_interval, @@ -205,6 +212,7 @@ defmodule Sanbase.AvailableMetrics do def apply_filters(metrics_list, filters) when is_list(metrics_list) do metrics_list |> reject_hidden_group_metrics() + |> reject_hidden_and_deprecated_metrics() |> maybe_apply_filter(:docs, filters) |> maybe_apply_filter(:only_with_docs, filters) |> maybe_apply_filter(:only_intraday_metrics, filters) @@ -225,6 +233,33 @@ defmodule Sanbase.AvailableMetrics do end) end + # The page lists what the API will actually serve. + # + # `is_hidden` means "keep out of metric lists", so it is dropped here. A + # `hard_deprecate_after` that has passed makes every request fail, so it is + # dropped too - `Sanbase.Metric.available_metrics/0` normally does that already, + # but it filters at refresh time into a cache with no TTL, so a date that passes + # between refreshes leaves the metric listed. Filtering again here is what keeps + # the page honest in that window, and it also catches a name that a code adapter + # keeps advertising while the registry deprecates it. + # + # A metric that is only `is_deprecated` is deliberately kept: it still returns + # data, with no end date, so it belongs in the inventory. Note that it is not in + # any package - `Bundle.PackageSnapshot.sellable?/1` rejects the flag - so the + # page shows it while nobody can buy it any more. + defp reject_hidden_and_deprecated_metrics(metrics) do + now = DateTime.utc_now() + + Enum.reject(metrics, fn metric -> + metric[:is_hidden] == true or hard_deprecated?(metric[:hard_deprecate_after], now) + end) + end + + defp hard_deprecated?(%DateTime{} = deprecate_after, now), + do: DateTime.before?(deprecate_after, now) + + defp hard_deprecated?(_deprecate_after, _now), do: false + # "with" | "without" | "all". `only_with_docs` is the older boolean form of the # same thing - the page sends `docs`, links and saved exports may still send the # checkbox. diff --git a/lib/sanbase/billing/plan/bundle/package_snapshot.ex b/lib/sanbase/billing/plan/bundle/package_snapshot.ex index a0fe771e74..45c9a50493 100644 --- a/lib/sanbase/billing/plan/bundle/package_snapshot.ex +++ b/lib/sanbase/billing/plan/bundle/package_snapshot.ex @@ -279,6 +279,17 @@ defmodule Sanbase.Billing.Plan.Bundle.PackageSnapshot do defp sellable?(%Registry{is_deprecated: true}), do: false defp sellable?(%Registry{is_hidden: true}), do: false + + # A `hard_deprecate_after` that has passed is enforced on every request by + # `Sanbase.Metric.hard_deprecated?/1`, which returns an error rather than data. + # Selling it would put a metric in a package that no customer can fetch, and + # `is_deprecated` is a separate column that is often left unset when the date is + # scheduled - so this cannot rely on the flag above. + # + # A date still in the future is left sellable: the metric works until then. + defp sellable?(%Registry{hard_deprecate_after: %DateTime{} = deprecate_after}), + do: DateTime.after?(deprecate_after, DateTime.utc_now()) + defp sellable?(%Registry{}), do: true defp sellable?(_), do: false diff --git a/test/sanbase/available_metrics/available_metrics_categorization_filter_test.exs b/test/sanbase/available_metrics/available_metrics_categorization_filter_test.exs index dc0c81e63d..5858622495 100644 --- a/test/sanbase/available_metrics/available_metrics_categorization_filter_test.exs +++ b/test/sanbase/available_metrics/available_metrics_categorization_filter_test.exs @@ -124,6 +124,23 @@ defmodule Sanbase.AvailableMetricsCategorizationFilterTest do assert result == ["also_sellable"] end + test "hides hidden metrics and the ones the API already refuses" do + metrics = [ + metric("live", []), + # Still serves data, with no end date, so it stays in the inventory even + # though it can no longer be sold. + metric("soft_deprecated", []) |> Map.put(:is_deprecated, true), + # Works until the date, so it is still listed. + metric("scheduled", []) |> Map.put(:hard_deprecate_after, ~U[2100-01-01 00:00:00Z]), + # Every request fails - listing it would advertise an error. + metric("hard_deprecated", []) |> Map.put(:hard_deprecate_after, ~U[2020-01-01 00:00:00Z]), + metric("internal", []) |> Map.put(:is_hidden, true) + ] + + assert AvailableMetrics.apply_filters(metrics, %{}) |> Enum.map(& &1.metric) == + ["live", "soft_deprecated", "scheduled"] + end + test "does not match category and group across different mappings" do metrics = [ metric("multi", [ diff --git a/test/sanbase/billing/plan/bundle/package_snapshot_test.exs b/test/sanbase/billing/plan/bundle/package_snapshot_test.exs index bbfd1d9479..f55af9fa22 100644 --- a/test/sanbase/billing/plan/bundle/package_snapshot_test.exs +++ b/test/sanbase/billing/plan/bundle/package_snapshot_test.exs @@ -86,6 +86,36 @@ defmodule Sanbase.Billing.Plan.Bundle.PackageSnapshotTest do assert contents["market"] == ["bundle_test_kept"] end + test "leaves out a metric whose hard_deprecate_after has passed", %{categories: categories} do + # The date is enforced per request by `Sanbase.Metric.hard_deprecated?/1`, and + # `is_deprecated` is a separate column that is usually left unset when a date + # is scheduled - so the snapshot has to read the date itself or it sells a + # metric the API refuses. + categorize("bundle_test_kept", categories["market"]) + + categorize( + %{metric: "bundle_test_hard_deprecated", hard_deprecate_after: ~U[2020-01-01 00:00:00Z]}, + categories["market"] + ) + + {:ok, contents} = PackageSnapshot.materialize() + + assert contents["market"] == ["bundle_test_kept"] + end + + test "keeps a metric whose hard_deprecate_after is still in the future", %{ + categories: categories + } do + categorize( + %{metric: "bundle_test_scheduled", hard_deprecate_after: ~U[2100-01-01 00:00:00Z]}, + categories["market"] + ) + + {:ok, contents} = PackageSnapshot.materialize() + + assert contents["market"] == ["bundle_test_scheduled"] + end + test "includes metrics served by adapter modules rather than the registry", %{ categories: categories } do