Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
43e97b1
Add OpenFeature component
Strech Oct 22, 2025
aa2fd5a
Add openfeature-sdk stub and fix component typing
Strech Oct 24, 2025
32ca2ff
Add testing matrix and new group
Strech Oct 28, 2025
9e5c4f5
Update component initialization
Strech Nov 4, 2025
cef56ef
Add InternalEvaluator class skeleton for functional flag evaluation
sameerank Nov 4, 2025
3272b08
Add JSON parsing validation to InternalEvaluator with error codes
sameerank Nov 5, 2025
64d0591
Standardize InternalEvaluator error messages with libdatadog
sameerank Nov 5, 2025
9e23207
Implement UFC configuration parsing with comprehensive Ruby data stru…
sameerank Nov 5, 2025
0d738fd
Implement flag lookup with libdatadog-compatible error handling
sameerank Nov 5, 2025
62c55e3
Use allocation and variation data in InternalEvaluator
sameerank Nov 5, 2025
c1ce9a9
Integrate user defaults through OpenFeature evaluation chain
sameerank Nov 5, 2025
2d7c83f
Implement allocation matching logic with time bounds and user defaults
sameerank Nov 5, 2025
c98fc66
Implement comprehensive rule evaluation for allocation target
sameerank Nov 5, 2025
b8cb103
Implement functional InternalEvaluator with 98.1% reference compatibiity
sameerank Nov 5, 2025
5250a58
Fix InternalEvaluator compatibility and resolve class conflicts
sameerank Nov 6, 2025
0e8164f
Align Ruby evaluator with libdatadog FFE interface contract
sameerank Nov 6, 2025
c6899b9
Refactor InternalEvaluator and remove deprecated Evaluator
sameerank Nov 6, 2025
1e1c2f9
Remove time parameter from get_assignment to align with libdatadog FFI
sameerank Nov 7, 2025
7991c83
Update InternalEvaluator to match NativeEvaluator schema and configur…
sameerank Nov 7, 2025
9ed6cd3
Update RBS type signatures for OpenFeature binding refactor
sameerank Nov 7, 2025
c5fbfc2
Add test coverage for InternalEvaluator using fixture files
sameerank Nov 7, 2025
0f61d96
Clarify UFC acronym throughout codebase for better developer understa…
sameerank Nov 10, 2025
7be46ca
Rename from_json to from_hash and reorder parameters
sameerank Nov 10, 2025
aae511e
Remove unused legacy format support and dead code
sameerank Nov 10, 2025
2179ba3
Improve Ruby idioms by using Hash#fetch with defaults
sameerank Nov 10, 2025
500cefb
Add validation for required variationType field
sameerank Nov 10, 2025
9bf8e60
Improve Variation class documentation
sameerank Nov 10, 2025
fa9c847
Use idiomatic Ruby constructors
sameerank Nov 10, 2025
882f01d
Remove redundant nil check in parse_timestamp (handled by else case)
sameerank Nov 10, 2025
547bf57
Use fetch for consistently present fields to enforce validation
sameerank Nov 10, 2025
18d1efc
Replace the end method definition with an alias for end_value
sameerank Nov 10, 2025
678ebde
Remove unnecessary parse_condition_value method and update RBS signat…
sameerank Nov 10, 2025
c7a1c00
Extract constant modules into separate files for better organization
sameerank Nov 10, 2025
5ec359f
Restore core OpenFeature evaluation functionality
sameerank Nov 10, 2025
6d60cf3
Clean up rebase artifacts and align with base branch
sameerank Nov 10, 2025
39ee406
More clean up rebase artifacts and align with base branch expectations
sameerank Nov 10, 2025
9e7ec9b
Remove default_value and time arguments from evaluator get_assignment…
sameerank Nov 10, 2025
641a6fb
Remove duplicate ResolutionDetails and consolidate to shared implemen…
sameerank Nov 10, 2025
0a8139c
Remove default_value parameter from OpenFeature evaluator methods
sameerank Nov 10, 2025
e86e4e2
Fix error message handling to align with libdatadog conventions
sameerank Nov 11, 2025
703eac6
Fix OpenFeature evaluator to align with libdatadog behavior and pass …
sameerank Nov 11, 2025
ea5beb4
Fix OpenFeature error_code logic for correct provider behavior
sameerank Nov 11, 2025
8352517
Add libdatadog FFI Reason enum support to OpenFeature binding
sameerank Nov 11, 2025
ec5e23d
Consolidate OpenFeature binding tests and complete libdatadog alignment
sameerank Nov 11, 2025
c62d6ef
Revert unnecessary provider error code conversion changes
sameerank Nov 11, 2025
f8d364f
Fix OpenFeature binding test failures after consolidation
sameerank Nov 11, 2025
f0d3309
Fix remaining OpenFeature test failures and complete integration
sameerank Nov 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion lib/datadog/open_feature/binding.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,9 @@ module Binding
end
end

require_relative 'binding/evaluator'
require_relative 'binding/resolution_details'
require_relative 'binding/internal_evaluator'
require_relative 'binding/configuration'

# Define alias for backward compatibility after InternalEvaluator is loaded
Datadog::OpenFeature::Binding::Evaluator = Datadog::OpenFeature::Binding::InternalEvaluator
Copy link
Author

Choose a reason for hiding this comment

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

The aspiration is that in #5007 when it is ready, we can just switch this to

Datadog::OpenFeature::Binding::Evaluator = Datadog::OpenFeature::Binding::NativeEvaluator

And then we're using the libdatadog FFE powered evaluator

18 changes: 18 additions & 0 deletions lib/datadog/open_feature/binding/assignment_reason.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# frozen_string_literal: true

module Datadog
module OpenFeature
module Binding
# Assignment reasons returned in ResolutionDetails
# Aligned with libdatadog FFI Reason enum
module AssignmentReason
TARGETING_MATCH = 'TARGETING_MATCH'
SPLIT = 'SPLIT'
STATIC = 'STATIC'
DEFAULT = 'DEFAULT' # For DefaultAllocationNull cases (matches libdatadog FFI Reason::Default)
DISABLED = 'DISABLED' # For FlagDisabled cases
ERROR = 'ERROR'
end
end
end
end
20 changes: 20 additions & 0 deletions lib/datadog/open_feature/binding/condition_operator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# frozen_string_literal: true

module Datadog
module OpenFeature
module Binding
# Condition operators for rule evaluation
module ConditionOperator
MATCHES = 'MATCHES'
NOT_MATCHES = 'NOT_MATCHES'
GTE = 'GTE'
GT = 'GT'
LTE = 'LTE'
LT = 'LT'
ONE_OF = 'ONE_OF'
NOT_ONE_OF = 'NOT_ONE_OF'
IS_NULL = 'IS_NULL'
end
end
end
end
252 changes: 252 additions & 0 deletions lib/datadog/open_feature/binding/configuration.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
# frozen_string_literal: true

require_relative 'variation_type'
require_relative 'condition_operator'
require_relative 'assignment_reason'

module Datadog
module OpenFeature
module Binding
# Represents a feature flag configuration
class Flag
attr_reader :key, :enabled, :variation_type, :variations, :allocations

def initialize(key:, enabled:, variation_type:, variations:, allocations:)
@key = key
@enabled = enabled
@variation_type = variation_type
@variations = Hash(variations)
@allocations = Array(allocations)
end

def self.from_hash(flag_data, key)
new(
key: key,
enabled: flag_data.fetch('enabled', false),
variation_type: flag_data.fetch('variationType'),
variations: parse_variations(flag_data.fetch('variations', {})),
allocations: parse_allocations(flag_data.fetch('allocations', []))
)
end

private

def self.parse_variations(variations_data)
variations_data.transform_values do |variation_data|
Variation.from_hash(variation_data)
end
end

def self.parse_allocations(allocations_data)
allocations_data.map { |allocation_data| Allocation.from_hash(allocation_data) }
end
end

# Represents a flag variation with a key for logging and a value for the application
class Variation
Copy link
Member

Choose a reason for hiding this comment

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

VariationValue?

Copy link
Author

@sameerank sameerank Nov 10, 2025

Choose a reason for hiding this comment

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

It contains a key (for logging) and a value (the data that the client needs from the flag)

"red": {
"key": "red",
"value": "red"
},

I've updated the comment since that seems to be causing confusion: 7d966c4

attr_reader :key, :value

def initialize(key:, value:)
@key = key
@value = value
end

def self.from_hash(variation_data)
new(
key: variation_data.fetch('key'),
value: variation_data.fetch('value')
)
end
end

# Represents an allocation rule with traffic splits
class Allocation
Copy link
Member

Choose a reason for hiding this comment

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

does it make sense to name this class AllocationRule, if it represents a rule?

Copy link
Author

@sameerank sameerank Nov 10, 2025

Choose a reason for hiding this comment

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

An allocation contains a few things: rules (matches deterministically), splits (matches randomly but consistently, using a hash function), if the flag assignment should be logged for experiment analysis. This is a pattern we are bringing to DD.

Also it'll be easier to see which fields in the flag configuration are being represented if the class and key names match.

Much of this is temporary. It will be replaced by a C binding to an evaluator in libdatadog and then the Ruby evaluation code can be deleted. In the meantime, I'm matching up the Ruby evaluator as closely as I can in structure and function to the one in Rust. We are hedging dependencies so we can get the end to end working asap

attr_reader :key, :rules, :start_at, :end_at, :splits, :do_log

def initialize(key:, rules: nil, start_at: nil, end_at: nil, splits:, do_log: true)
@key = key
@rules = rules
@start_at = start_at
@end_at = end_at
@splits = Array(splits)
@do_log = do_log
end

def self.from_hash(allocation_data)
new(
key: allocation_data.fetch('key'),
rules: parse_rules(allocation_data['rules']),
start_at: parse_timestamp(allocation_data['startAt']),
end_at: parse_timestamp(allocation_data['endAt']),
splits: parse_splits(allocation_data.fetch('splits', [])),
do_log: allocation_data.fetch('doLog', true)
)
end

private

def self.parse_rules(rules_data)
return nil if rules_data.nil? || rules_data.empty?

rules_data.map { |rule_data| Rule.from_hash(rule_data) }
end

def self.parse_splits(splits_data)
splits_data.map { |split_data| Split.from_hash(split_data) }
end

def self.parse_timestamp(timestamp_data)
# Handle both Unix timestamps and ISO8601 strings
case timestamp_data
when Numeric
Time.at(timestamp_data)
when String
Time.parse(timestamp_data)
else
nil
end
rescue StandardError
nil
end
end

# Represents a traffic split within an allocation
class Split
Copy link
Member

Choose a reason for hiding this comment

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

TrafficSplit?

Copy link
Author

Choose a reason for hiding this comment

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

I think it'll be easier to see how this corresponds to the fields with the same names in the JSON if the key and class names match, e.g.

Copy link
Member

Choose a reason for hiding this comment

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

Tbh I think if you are adding an abstraction over this data to avoid working with a big Hash directly, it makes no sense to duplicate key names 1:1 to class names. The class names should make sense on their own, since it helps with overall code readability. But I guess this is your decision in the end, especially if this code is temporary as you mentioned.

attr_reader :shards, :variation_key, :extra_logging

def initialize(shards:, variation_key:, extra_logging: nil)
@shards = Array(shards)
@variation_key = variation_key
@extra_logging = Hash(extra_logging)
end

def self.from_hash(split_data)
new(
shards: parse_shards(split_data.fetch('shards', [])),
variation_key: split_data.fetch('variationKey'),
extra_logging: split_data.fetch('extraLogging', {})
)
end

private

def self.parse_shards(shards_data)
shards_data.map { |shard_data| Shard.from_hash(shard_data) }
end
end

# Represents a shard configuration for traffic splitting
class Shard
Copy link
Member

Choose a reason for hiding this comment

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

I am not sure about this name - it's called Shard, but has a property called total_shards?

Copy link
Author

Choose a reason for hiding this comment

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

Another case of exactly matching the key name in the JSON

attr_reader :salt, :total_shards, :ranges

def initialize(salt:, total_shards:, ranges:)
@salt = salt
@total_shards = total_shards
@ranges = Array(ranges)
end

def self.from_hash(shard_data)
new(
salt: shard_data.fetch('salt'),
total_shards: shard_data.fetch('totalShards'),
ranges: parse_ranges(shard_data.fetch('ranges', []))
)
end

private

def self.parse_ranges(ranges_data)
ranges_data.map { |range_data| ShardRange.from_hash(range_data) }
end
end

# Represents a shard range for traffic allocation
class ShardRange
attr_reader :start, :end_value

def initialize(start:, end_value:)
@start = start
@end_value = end_value
end

def self.from_hash(range_data)
new(
start: range_data.fetch('start'),
end_value: range_data.fetch('end')
)
end

# Alias because "end" is a reserved keyword in Ruby
alias_method :end, :end_value
end

# Represents a targeting rule
class Rule
Copy link
Member

Choose a reason for hiding this comment

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

TargetingRule?

Copy link
Author

Choose a reason for hiding this comment

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

Also matching the field name in the universal flag configuration

attr_reader :conditions

def initialize(conditions:)
@conditions = Array(conditions)
end

def self.from_hash(rule_data)
new(
conditions: parse_conditions(rule_data.fetch('conditions', []))
)
end

private

def self.parse_conditions(conditions_data)
conditions_data.map { |condition_data| Condition.from_hash(condition_data) }
end
end

# Represents a single condition within a rule
class Condition
attr_reader :attribute, :operator, :value

def initialize(attribute:, operator:, value:)
@attribute = attribute
@operator = operator
@value = value
end

def self.from_hash(condition_data)
new(
attribute: condition_data.fetch('attribute'),
operator: condition_data.fetch('operator'),
value: condition_data.fetch('value')
)
end

end

# Main configuration container
class Configuration
attr_reader :flags, :schema_version

def initialize(flags:, schema_version: nil)
@flags = Hash(flags)
@schema_version = schema_version
end

def self.from_hash(config_data)
flags_data = config_data.fetch('flags', {})

parsed_flags = flags_data.transform_values do |flag_data|
Flag.from_hash(flag_data, flag_data['key'] || '')
end

new(
flags: parsed_flags,
schema_version: config_data['schemaVersion']
)
end

def get_flag(flag_key)
@flags.values.find { |flag| flag.key == flag_key }
end
end
end
end
end
44 changes: 0 additions & 44 deletions lib/datadog/open_feature/binding/evaluator.rb

This file was deleted.

Loading
Loading