diff --git a/CHANGES.txt b/CHANGES.txt index a9e4fc42..ecfe21a6 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,7 +1,11 @@ CHANGES +8.10.1 (Jan 28, 2025) +- Fixed rule-based segment matcher to exit when a conition is met. +- Fixed impressions properties format in redis mode. + 8.10.0 (Nov 28, 2025) -- Replaced socketry gem used in streaming feature with built-in socket lib. +- Updated socketry gem used in streaming feature with built-in socket lib. 8.9.0 (Oct 8, 2025) - Added new configuration for Fallback Treatments, which allows setting a treatment value and optional config to be returned in place of "control", either globally or by flag. Read more in our docs. diff --git a/LICENSE b/LICENSE index df08de3f..0f9e8a59 100644 --- a/LICENSE +++ b/LICENSE @@ -1,13 +1,169 @@ -Copyright © 2025 Split Software, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + 1. Definitions. + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + END OF TERMS AND CONDITIONS + APPENDIX: How to apply the Apache License to your work. + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + Copyright [yyyy] [name of copyright owner] + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/NOTICE.txt b/NOTICE.txt new file mode 100644 index 00000000..586e4ca5 --- /dev/null +++ b/NOTICE.txt @@ -0,0 +1,5 @@ +Harness Feature Management JavaScript SDK Copyright 2024-2026 Harness Inc. + +This product includes software developed at Harness Inc. (https://harness.io/). + +This product includes software originally developed by Split Software, Inc. (https://www.split.io/). Copyright 2016-2024 Split Software, Inc. diff --git a/lib/splitclient-rb/cache/repositories/impressions/redis_repository.rb b/lib/splitclient-rb/cache/repositories/impressions/redis_repository.rb index 0a59c123..d9f8a098 100644 --- a/lib/splitclient-rb/cache/repositories/impressions/redis_repository.rb +++ b/lib/splitclient-rb/cache/repositories/impressions/redis_repository.rb @@ -14,6 +14,7 @@ def initialize(config) def add_bulk(impressions) impressions_json = impressions.map do |impression| + impression[:i][:properties] = impression[:i][:properties].to_json.to_s unless impression[:i][:properties].nil? impression.to_json end diff --git a/lib/splitclient-rb/engine/matchers/rule_based_segment_matcher.rb b/lib/splitclient-rb/engine/matchers/rule_based_segment_matcher.rb index a5463d0a..2443e258 100644 --- a/lib/splitclient-rb/engine/matchers/rule_based_segment_matcher.rb +++ b/lib/splitclient-rb/engine/matchers/rule_based_segment_matcher.rb @@ -30,15 +30,18 @@ def match?(args) return false unless check_excluded_segments(rule_based_segment, key, args) - matches = false + matched = false rule_based_segment[:conditions].each do |c| condition = SplitIoClient::Condition.new(c, @config) next if condition.empty? - matches = Helpers::EvaluatorHelper.matcher_type(condition, @segments_repository, @rule_based_segments_repository).match?(args) + matched = Helpers::EvaluatorHelper.matcher_type(condition, @segments_repository, @rule_based_segments_repository).match?(args) + + break if matched end - @logger.debug("[InRuleSegmentMatcher] #{@segment_name} is in rule based segment -> #{matches}") - matches + + @logger.debug("[InRuleSegmentMatcher] #{@segment_name} is in rule based segment -> #{matched}") + matched end private diff --git a/lib/splitclient-rb/sse/event_source/client.rb b/lib/splitclient-rb/sse/event_source/client.rb index 08ac28ee..bb07d76a 100644 --- a/lib/splitclient-rb/sse/event_source/client.rb +++ b/lib/splitclient-rb/sse/event_source/client.rb @@ -43,11 +43,11 @@ def close(status = nil) end @config.logger.debug("Closing SSEClient socket") + push_status(status) @connected.make_false @socket.sync_close = true if @socket.is_a? OpenSSL::SSL::SSLSocket @socket.close @config.logger.debug("SSEClient socket state #{@socket.state}") if @socket.is_a? OpenSSL::SSL::SSLSocket - push_status(status) rescue StandardError => e @config.logger.error("SSEClient close Error: #{e.inspect}") end diff --git a/lib/splitclient-rb/version.rb b/lib/splitclient-rb/version.rb index d45f095e..82ab6583 100644 --- a/lib/splitclient-rb/version.rb +++ b/lib/splitclient-rb/version.rb @@ -1,3 +1,3 @@ module SplitIoClient - VERSION = '8.10.0' + VERSION = '8.10.1' end diff --git a/spec/cache/senders/impressions_formatter_spec.rb b/spec/cache/senders/impressions_formatter_spec.rb index 6c649a48..79b20c03 100644 --- a/spec/cache/senders/impressions_formatter_spec.rb +++ b/spec/cache/senders/impressions_formatter_spec.rb @@ -92,29 +92,55 @@ ] ) - expect(formatted_impressions.find { |i| i[:f] == :foo2 }[:i]).to match_array( - [ - { - k: 'matching_key2', - t: 'off', - m: 1_478_113_518_285, - b: 'foo2', - r: 'custom_label2', - c: 123_499, - pt: nil - }, - { - k: 'matching_key3', - t: 'off', - m: 1_478_113_518_900, - b: nil, - r: nil, - c: nil, - pt: nil, - properties: '{"prop":"val"}' - } - ] - ) + if cache_adapter == :redis + expect(formatted_impressions.find { |i| i[:f] == :foo2 }[:i]).to match_array( + [ + { + k: 'matching_key2', + t: 'off', + m: 1_478_113_518_285, + b: 'foo2', + r: 'custom_label2', + c: 123_499, + pt: nil + }, + { + k: 'matching_key3', + t: 'off', + m: 1_478_113_518_900, + b: nil, + r: nil, + c: nil, + pt: nil, + properties: '"{\"prop\":\"val\"}"' + } + ] + ) + else + expect(formatted_impressions.find { |i| i[:f] == :foo2 }[:i]).to match_array( + [ + { + k: 'matching_key2', + t: 'off', + m: 1_478_113_518_285, + b: 'foo2', + r: 'custom_label2', + c: 123_499, + pt: nil + }, + { + k: 'matching_key3', + t: 'off', + m: 1_478_113_518_900, + b: nil, + r: nil, + c: nil, + pt: nil, + properties: '{"prop":"val"}' + } + ] + ) + end end it 'filters out impressions with the same key/treatment' do diff --git a/spec/cache/senders/impressions_sender_spec.rb b/spec/cache/senders/impressions_sender_spec.rb index ccb5b885..3db870f7 100644 --- a/spec/cache/senders/impressions_sender_spec.rb +++ b/spec/cache/senders/impressions_sender_spec.rb @@ -55,6 +55,8 @@ end it 'post impressions with corresponding impressions metadata' do + skip "Test is not relevant for redis" if cache_adapter == :redis + stub_request(:post, 'https://events.split.io/api/testImpressions/bulk') .to_return(status: 200, body: 'ok') @@ -99,6 +101,7 @@ end it 'calls #post_impressions upon destroy' do + skip "Test is not relevant for redis" if cache_adapter == :redis stub_request(:post, 'https://events.split.io/api/testImpressions/bulk').to_return(status: 200, body: '') sender.call diff --git a/spec/engine/matchers/rule_based_segment_matcher_spec.rb b/spec/engine/matchers/rule_based_segment_matcher_spec.rb index 4f7786e4..6034dfc3 100644 --- a/spec/engine/matchers/rule_based_segment_matcher_spec.rb +++ b/spec/engine/matchers/rule_based_segment_matcher_spec.rb @@ -177,6 +177,61 @@ matcher = described_class.new(segments_repository, rbs_repositoy, 'sample_rule_based_segment', config) expect(matcher.match?(value: 'bilal@split.io', attributes: {'email': 'bilal@split.io'})).to be true expect(matcher.match?(value: 'bilal', attributes: {'email': 'bilal'})).to be false - end + end + + it 'return true if has multiple conditions' do + rbs_repositoy = SplitIoClient::Cache::Repositories::RuleBasedSegmentsRepository.new(config) + rbs = { + :name => 'sample_rule_based_segment', + :trafficTypeName => 'tt_name_1', + :conditions => [ + { + :matcherGroup => { + :combiner => "AND", + :matchers => [ + { + :matcherType => "WHITELIST", + :negate => false, + :userDefinedSegmentMatcherData => nil, + :whitelistMatcherData => { + :whitelist => [ + "bilal" + ] + }, + :unaryNumericMatcherData => nil, + :betweenMatcherData => nil + } + ] + } + }, + { + :matcherGroup => { + :combiner => "AND", + :matchers => [ + { + :matcherType => "WHITELIST", + :negate => false, + :userDefinedSegmentMatcherData => nil, + :whitelistMatcherData => { + :whitelist => [ + "mauro" + ] + }, + :unaryNumericMatcherData => nil, + :betweenMatcherData => nil + } + ] + } + } + ], + :excluded => {:keys => [], :segments => []} + } + + rbs_repositoy.update([rbs], [], -1) + matcher = described_class.new(segments_repository, rbs_repositoy, 'sample_rule_based_segment', config) + expect(matcher.match?(value: 'mauro', attributes: {})).to be true + expect(matcher.match?(value: 'bilal', attributes: {})).to be true + expect(matcher.match?(value: 'nicolas', attributes: {})).to be false + end end end \ No newline at end of file diff --git a/spec/integrations/in_memory_client_spec.rb b/spec/integrations/in_memory_client_spec.rb index 6802710d..b65a8fd5 100644 --- a/spec/integrations/in_memory_client_spec.rb +++ b/spec/integrations/in_memory_client_spec.rb @@ -1401,8 +1401,8 @@ client_rbs = factory_rbs.client client_rbs.block_until_ready - expect(client_rbs.get_treatment('bilal@split.io', 'rbs_feature_flag', {:email => 'bilal@split.io'})).to eq('on') - expect(client_rbs.get_treatment('mauro@split.io', 'rbs_feature_flag', {:email => 'mauro@split.io'})).to eq('off') + expect(client_rbs.get_treatment('bilal', 'rbs_feature_flag', {:email => 'bilal@split.io'})).to eq('on') + expect(client_rbs.get_treatment('mauro', 'rbs_feature_flag', {:email => 'mauro@split.io'})).to eq('off') end end diff --git a/spec/test_data/rule_based_segments/rule_base_segments.json b/spec/test_data/rule_based_segments/rule_base_segments.json index deafbdc2..6a9b4bd4 100644 --- a/spec/test_data/rule_based_segments/rule_base_segments.json +++ b/spec/test_data/rule_based_segments/rule_base_segments.json @@ -78,7 +78,7 @@ "name": "dependent_rbs", "status": "ACTIVE", "trafficTypeName": "user", - "excluded":{"keys":["mauro@split.io","gaston@split.io"],"segments":[]}, + "excluded":{"keys":["mauro","gaston@split.io"],"segments":[]}, "conditions": [ { "conditionType": "WHITELIST", @@ -100,6 +100,27 @@ } ] } + }, + { + "conditionType": "WHITELIST", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": "email" + }, + "matcherType": "ENDS_WITH", + "negate": false, + "whitelistMatcherData": { + "whitelist": [ + "@harness.io" + ] + } + } + ] + } } ]}, {