-
Notifications
You must be signed in to change notification settings - Fork 28
Remove Her dependency #142
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
base: master
Are you sure you want to change the base?
Conversation
| def save | ||
| return false unless valid? | ||
|
|
||
| persist! | ||
| true | ||
| rescue Faraday::UnprocessableEntityError | ||
| errors.add(:base, 'The HTTP request failed with a 422 status code') | ||
| false | ||
| end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note: The 422 response contains the errors that occurred, and Her would normally assign that to the record. I don't think we're relying on that functionality, so I did the bare minimum here.
| def initialize(...) | ||
| super | ||
| clear_changes_information | ||
| end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This resource uses ActiveModel::Dirty to implement variant_changed?. It'd probably be better to not rely on AM::Dirty, but I wanted to maintain the current behavior as much as possible.
| end | ||
|
|
||
| def save! | ||
| save or raise(ActiveModel::ValidationError, self) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
rare or spotted in the wild! but is it worth using it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ha, this is one of those rare cases where you're supposed to use or. I restrict my usage to or raise and or return.
| def _assign_attribute(name, value) | ||
| super | ||
| rescue ActiveModel::UnknownAttributeError | ||
| # Don't raise when we encounter an unknown attribute. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because there was an existing test for this behavior, but also because it makes sense with the way APIs are versioned.
Also, if we don't allow unknown fields, any non-breaking change (e.g. new field is added) to the API will break the client.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We do have an API versioning strategy, so the addition of a new field could (and probably should) fall under a new version release. But I can understand not introducing additional strictness as part of this PR.
| def assigned_at | ||
| original = super | ||
| if original.blank? || !original.respond_to?(:in_time_zone) | ||
| nil | ||
| else | ||
| original.in_time_zone rescue nil # rubocop:disable Style/RescueModifier | ||
| end | ||
| end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I assume this is more or less what :datetime buys us here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup, exactly. There's a test that covers this, too.
|
|
||
| def request(method:, path:, fake:, body: nil, headers: nil) | ||
| # Ensure that fake responses are consistent with real responses | ||
| return JSON.parse(JSON.generate(fake)) if fake? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 so this is essentially the fakeable_her replacement API -- just pass in fake: with every request, which will be ignored if we're not in fake mode.
| def visitor=(value) | ||
| @visitor = TestTrack::Remote::Visitor.new(value).to_visitor | ||
| end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So the assignment here depends on assign_attributes (below) which is why this method needs to be public (?) even if it's not intended for anything external to be able to assign it.
Also value here is really hash with :id and :assignments -- wondering if there's a code/name change that would help communicate that. (It's basically private though, so it's more that it jumped out me as a public API and I was trying to figure out what uses it.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Renamed to visitor_attributes
| def self.destroy_existing(id) | ||
| TestTrack::Client.request( | ||
| method: :delete, | ||
| path: "api/v1/split_configs/#{id}", | ||
| fake: nil | ||
| ) | ||
|
|
||
| nil | ||
| end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What calls this method?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
test_track_rails_client/app/models/test_track/config_updater.rb
Lines 16 to 22 in 109bcb0
| def drop_split(name) | |
| TestTrack::Remote::SplitConfig.destroy_existing(name) | |
| splits.except!(name.to_s) | |
| persist_schema! | |
| end |
| end | ||
|
|
||
| def variant_details=(values) | ||
| super(values.map(&:symbolize_keys)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should a symbolize_keys or deep_symbolize_keys be baked into the way that TestTrack::Client.request returns results?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I looked into that. The Faraday::Response::Json middleware does allow you to specify parser_options: { symbolize_names: true }.
This project is been pretty inconsistent with usage of symbol versus string keys. Fakes mostly have symbol keys. Some of our stubs mix string/symbol keys. Splits have string keys (deep). Variant details use symbol keys (shallow).
Using symbolize_names actually caused more problems than it solved, for whatever reason.
| def assignment_details=(values) | ||
| assignment_details = values.map do |value| | ||
| TestTrack::Remote::AssignmentDetail.new(value) | ||
| end | ||
|
|
||
| super(assignment_details) | ||
| end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool, so just handling has_many mappings on assignment. 👍
| 'weights' => { variant.to_s => 100 }, | ||
| 'feature_gate' => split_name.end_with?('_enabled') | ||
| } | ||
| assignments << { split_name:, variant: variant.to_s, unsynced: false } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
still symbols here but needed strings above?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, this change might not be necessary now that I'm doing JSON.parse(JSON.generate(data)). Let me take a peek.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, this change isn't actually necessary, so I could revert it, but I'd prefer to keep it.
This helper is building stubs. The stubbed return values should be consistent with the real return values.
TestTrack::Remote::Visitor.fake_instance_attributesreturns a hash with deeply symbolized keys.TestTrack::Remote::SplitRegistry.fake_instance_attributesreturns a hash with deeply stringified keys.
Our usages of string vs symbol keys are really inconsistent.
$ spec/dummy/bin/rails c
Loading development environment (Rails 8.0.1)
[1] pry(main)> TestTrack::Remote::Visitor.fake_instance_attributes(nil)
=> {:id=>"fake_visitor_id", :assignments=>[{:split_name=>"split_1", :variant=>"true", :context=>"context", :unsynced=>false}, {:split_name=>"split_2", :variant=>"true", :context=>"context", :unsynced=>false}]}
[2] pry(main)> TestTrack::Remote::SplitRegistry.fake_instance_attributes(nil)
=> {"splits"=>{"banner_color"=>{"weights"=>{"blue"=>100, "red"=>0, "white"=>0}, "feature_gate"=>false}, "buy_one_get_one_promotion_enabled"=>{"weights"=>{"false"=>100, "true"=>0}, "feature_gate"=>true}, "decided_split"=>{"weights"=>{"treatment"=>100, "control"=>0}, "feature_gate"=>false}}, "experience_sampling_weight"=>1}
| def build_connection(url:, options: {}) | ||
| Faraday.new(url) do |conn| | ||
| conn.use Faraday::Request::Json | ||
| conn.use Faraday::Response::Json, content_type: [] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remind me what content_type: [] does here -- it makes it so the server doesn't strictly need to return with a JSON content type for the response to be cast to JSON?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right content_type: [] means "just assume it's JSON, even if the Content-Type header doesn't specify". It's more consistent with the old behavior from FaradayMiddleware::ParseJson.
I realize that the test track server is always going to specify the correct content type. The problem is that our tests using stub_request won't, and there's a lot of them.
| attr_writer :connection | ||
|
|
||
| def fake? | ||
| !TestTrack.enabled? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Prior definition of faked? was:
def faked?
!ENV["#{service_name.to_s.upcase}_ENABLED"] && (Rails.env.development? || Rails.env.test?)
end
Unless we're changing the definition of enabled? somewhere, seems like we'd be changing the definition of fake?/faked? to ignore Rails.env when computing a default, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
RemoteModel was overriding the definition from FakeableHer here:
| def faked? | |
| !TestTrack.enabled? | |
| end |
Summary
This PR removes vendored gems. We do this by replacing all usages of Her with Faraday.
This PR does contain breaking changes:
use_apiis no longer available. If you need to customize the HTTP connection, you can assignTestTrack::Client.connectiondirectly. Consider usingTestTrack::Client.build_connection.test_track_rails_clientto function. So, if consumers of this gem were invoking methods onTestTrack::Remote::*models directly, compatibility is not guaranteed.None of these functionalities were documented, so the risk is pretty low.
Other Information
The test suite for this gem makes extensive use of stubbing. It's hard to be completely confident that this change represents parity.
/domain @Betterment/test_track_core
/platform @Betterment/test_track_core