-
Notifications
You must be signed in to change notification settings - Fork 26
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
Remove Her dependency #142
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 |
@@ -39,4 +45,8 @@ def self.fake_variant_details | |||
} | |||
] | |||
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') | ||
'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_attributes
returns a hash with deeply symbolized keys.TestTrack::Remote::SplitRegistry.fake_instance_attributes
returns 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_api
is no longer available. If you need to customize the HTTP connection, you can assignTestTrack::Client.connection
directly. Consider usingTestTrack::Client.build_connection
.test_track_rails_client
to 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