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: Create a data structure to populate the system status widget #2330

Open
wants to merge 35 commits into
base: main
Choose a base branch
from

Conversation

joshlarson
Copy link
Contributor

@joshlarson joshlarson commented Jan 14, 2025

@joshlarson joshlarson changed the base branch from main to jdl/system-status-alert-retrieval January 14, 2025 19:01
Copy link
Contributor

@anthonyshull anthonyshull left a comment

Choose a reason for hiding this comment

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

Could probably be cleaned up a bit. One suggestion would be to write comments for all of the private functions. I find that it helps add clarity when I have to write narrative descriptions.

lib/dotcom/system_status/groups.ex Show resolved Hide resolved
lib/dotcom/system_status/groups.ex Outdated Show resolved Hide resolved
lib/dotcom/system_status/groups.ex Outdated Show resolved Hide resolved
lib/dotcom/system_status/groups.ex Outdated Show resolved Hide resolved
test/dotcom/system_status/groups_test.exs Outdated Show resolved Hide resolved
lib/dotcom/system_status/groups.ex Outdated Show resolved Hide resolved
lib/dotcom/system_status/groups.ex Outdated Show resolved Hide resolved
lib/dotcom/system_status/groups.ex Outdated Show resolved Hide resolved
@joshlarson joshlarson force-pushed the jdl/system-status-flowchart branch from 9e706d9 to 0a31d30 Compare January 16, 2025 23:24
@joshlarson joshlarson force-pushed the jdl/system-status-alert-retrieval branch from a12cad9 to 42f225d Compare January 17, 2025 21:38
@joshlarson joshlarson force-pushed the jdl/system-status-flowchart branch from 0a31d30 to 58b5500 Compare January 21, 2025 18:08
Base automatically changed from jdl/system-status-alert-retrieval to main January 21, 2025 22:19
@joshlarson joshlarson force-pushed the jdl/system-status-flowchart branch from 58b5500 to c7e9dee Compare January 22, 2025 01:00
Comment on lines 90 to 94
%{
statuses: [%{time: nil, description: "Normal Service"}],
sub_routes: [],
route_id: ^route_id
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

suggestion(non-blocking): This could read real nice broken out into a function, like Enum.all?(lines, &shows_normal_service?/1) which validates the nil time and "Normal Service" text (though I suppose you'd want a separate check to verify the correct route IDs involved, maybe)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Question: I've given the tests a pretty thorough once-over (actually more like a twice- or thrice- over). Do you think this is still necessary?

Comment on lines 8 to 12
# &groups/2 returns an ordered data structure, sorted in the order
# given by `@lines`.
Copy link
Collaborator

Choose a reason for hiding this comment

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

This comment should be in a @doc above the def line

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I should restructure this comment to make it clearer that it's actually about @lines, not groups/2

@routes ["Blue", "Mattapan", "Orange", "Red"] ++ @green_line_branches

def groups(alerts, time) do
grouped_alerts = Map.new(@routes, &{&1, alerts_for_line(alerts, &1)})
Copy link
Collaborator

Choose a reason for hiding this comment

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

That function should be alerts_for_route if it operates per-route, no?

I also wonder if something in the Alerts.Match module might meet your needs, what if Alerts.Match.match(alerts, %InformedEntity{route: &1})?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That function should be alerts_for_route if it operates per-route, no?

Yep! Done.

I also wonder if something in the Alerts.Match module might meet your needs, what if Alerts.Match.match(alerts, %InformedEntity{route: &1})?

Ugh - I agree, but I've been struggling to actually get Alerts.Match to work, and I'm not sure exactly why. I still think it's worth using the machinery that already exists, but would you mind if I did that as a follow-up instead?

@joshlarson joshlarson force-pushed the jdl/system-status-flowchart branch from 7a900f9 to 675cd71 Compare January 23, 2025 19:41
@joshlarson joshlarson marked this pull request as ready for review January 23, 2025 21:51
@joshlarson joshlarson requested a review from a team as a code owner January 23, 2025 21:51
@joshlarson joshlarson marked this pull request as draft January 24, 2025 22:50
@joshlarson
Copy link
Contributor Author

Pulled back into draft because I realized that the data structure isn't quite the right shape to accommodate the desired frontend

@joshlarson joshlarson marked this pull request as ready for review January 27, 2025 22:17
Copy link
Contributor

@anthonyshull anthonyshull left a comment

Choose a reason for hiding this comment

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

Overall, I don't like the implicit use of nil as a time in order to try to simplify logic. It makes for using functions that purportedly take a time, but don't really. You could utilize module attributes a lot more to hardcode a lot less.

Comment on lines +615 to +631
defp alert(opts) do
route_id = opts |> Keyword.fetch!(:route_id)
effect = opts[:effect] || Faker.Util.pick(@effects)
active_period = opts |> Keyword.fetch!(:active_period)

Alert.build(:alert,
effect: effect,
informed_entity:
InformedEntitySet.build(:informed_entity_set,
route: route_id,
entities: [
InformedEntity.build(:informed_entity, route: route_id)
]
),
active_period: active_period
)
end
Copy link
Contributor

Choose a reason for hiding this comment

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

Any code that goes toward building an alert should go in the alerts factory.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I sort of see that, although exactly what belongs in a factory versus just here eludes me at the moment.

The problem is that I don't want to have to copy-pasta the informed_entity / informed_entity_set nesting over and over again, but I'm not sure that it makes sense to pull this into a factory as-is either.

There probably is a way to do it gracefully, but unless we can get that taken care of real fast, I think I'll ask that we tackle it as a follow-up, since this work is blocking the rest of the project.

Copy link
Contributor

Choose a reason for hiding this comment

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

You can see in this factory: https://github.com/mbta/dotcom/pull/2336/files#diff-8fbc124c966696c04346223e24b3cc686bb63d07f0f3a35316e93f8dd30c6384 how I separated _factory functions from other functions.

Comment on lines +10 to +12
@all_rail_lines ["Blue", "Green", "Orange", "Red"]
@green_line_branches ["Green-B", "Green-C", "Green-D", "Green-E"]
@heavy_rail_lines ["Blue", "Orange", "Red"]
Copy link
Contributor

Choose a reason for hiding this comment

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

We can export these out of the module.

defmodule Dotcom.SystemStatus.Groups do
  @heavy_rail_lines ~w[Blue Orange Red]
  @all_rail_lines @heavy_rails_lines ++ ~w[Green]
  
  def heavy_rail_lines, do: @heavy_rail_lines
  
  def all_rail_lines, do: @all_rail_lines
end

defmodule Dotcom.SystemStatus.GroupsTest do
  describe "groups/2" do
    test "sorts groups by heavy rail first" do
      # Exercise
      groups = Group.groups([], time_today())
      
      # Verify
      route_ids = groups |> Enum.map(&1.route_id)
      heavy_rail_lines = heavy_rail_lines()
      assert Enum.take(route_ids, Kernel.length(heavy_rail_lines)) == heavy_rail_lines
    end
  end
end

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I see that that's possible, but what is the benefit?

My problem is that it makes the tests a bit more tautological - instead of the assertion coming from the tests, the assertion comes from a mix of the source code and the tests, which means that the tests don't validate a user-facing behavior so much as validating that the code is interacting with its own internal constants.

I especially feel that way about this suggestion because the constants you mentioned aren't present in the source file, because I didn't need them. I would be exporting an internal constant that the source file doesn't even need for the sake of the tests, which is the kind of refactor that I've usually considered an antipattern.

I'll think on it for a little longer, but that's my first impression.

Copy link
Contributor

Choose a reason for hiding this comment

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

You don't necessarily have to have the heavy rail lines in a module attribute, though I think you did before. If you're getting them from somewhere else then you can get them there in the test.

Copy link
Contributor

Choose a reason for hiding this comment

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

Looks like you're just using @lines now. So, you can export that.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

...though I think you did before.

This is exactly why I don't want to export the module attributes. I did a pretty hefty refactor to Groups.groups/2 that changed which module attributes existed, and that refactor would have been harder if I had been exporting them. They're implementation details, and they change during refactors.

I see that that's possible, but what is the benefit?

Bump on this question, which kind of feels like it makes all the difference.

Copy link
Contributor

Choose a reason for hiding this comment

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

The benefit is that the test is more communicative.


# Translates an alert to a status:
# - The effect is humanized into a description for the status.
# - If the alert's already active, `time` is set to `nil`.
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not clear on why setting the time to nil means that it is active.

Copy link
Contributor Author

@joshlarson joshlarson Jan 28, 2025

Choose a reason for hiding this comment

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

The way this will be rendered in the frontend will be something like:

case time do
  nil -> description
  _ -> "#{time}: #{description}"
end

Thinking about other ways of representing this.... maybe prefix instead of time, since in a certain sense, that's actually what it is /slash/ how it will be used?

# If there are no alerts, then we want a single status indicating
# "Normal Service".
defp alerts_to_statuses_naive([], _time) do
[%{description: "Normal Service", time: nil}]
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there ever a case where the description is "Normal Service" and the time is not nil? Also, I see "Normal Service" in multiple places in this module and in the tests. Why not make it a module attribute and export it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is there ever a case where the description is "Normal Service" and the time is not nil?

I don't think so.

Also, I see "Normal Service" in multiple places in this module and in the tests. Why not make it a module attribute and export it?

Touché, although I'm not keen on exporting it for the same reason that I mentioned here.

Comment on lines +342 to +347
case alert.effect do
:delay -> "Delays"
:shuttle -> "Shuttle Buses"
:station_closure -> "Station Closure"
:suspension -> "Suspension"
end
Copy link
Contributor

Choose a reason for hiding this comment

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

This could be a module attribute of a map or a function using pattern matching. What happens if the alert.effect isn't in this list?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This could be a module attribute of a map or a function using pattern matching.

True. This felt pretty much equivalent to both of those 🤷‍♂️.

What happens if the alert.effect isn't in this list?

If the alert effect isn't in this list, that's an error that I'd probably want to see in Sentry. How do we typically handle this kind of thing?

Copy link
Contributor

Choose a reason for hiding this comment

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

Usually pattern matching and the fallback logs the error with the argument.

Comment on lines +368 to +375
# Returns true if the active period ends before the time given. An
# end-time of false indicates an indefinite active period, which
# never ends.
defp ends_before?({_start_time, nil}, _time), do: false
defp ends_before?({_start_time, end_time}, time), do: Timex.before?(end_time, time)

# Returns true if the active period starts before the time given.
defp starts_before?({start_time, _end_time}, time), do: Timex.before?(start_time, time)
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure we need these since they just wrap Timex functions.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ends_before? is needed, but maybe starts_before? isn't. It looked a bit odd to me to extract one and not the other, but I guess I can inline starts_before?.

Comment on lines +426 to +427
defp stringify_time(nil), do: nil
defp stringify_time(time), do: Util.kitchen_downcase_time(time)
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't like the fact that stringify_time takes something that isn't a time and returns something that isn't a string.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Do you have a better name in mind? maybe_time_to_prefix (assuming this suggestion)?

@@ -0,0 +1,465 @@
defmodule Dotcom.SystemStatus.Groups do
@moduledoc """

Copy link
Contributor

Choose a reason for hiding this comment

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

We don't have these spaces in the moduledoc of any other modules.

Comment on lines +16 to +17

Returns a data structure that can be used in the system status
Copy link
Contributor

Choose a reason for hiding this comment

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

We don't use this empty line in docs anywhere else. We shouldn't describe this module as being for use in front-end component. It should be useful on its own. Something like takes alerts groups them in order to communicate system status of each route.

:suspension -> "Suspension"
end

time = future_start_time(alert.active_period, time)
Copy link
Collaborator

Choose a reason for hiding this comment

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

question: Does Alerts.Match.any_time_match?/2 do the same thing?

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.

3 participants