Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions lib/open_feature/sdk/api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
require_relative "client_metadata"
require_relative "hooks"
require_relative "client"
require_relative "tracking_event_details"
require_relative "provider"

module OpenFeature
Expand Down
16 changes: 16 additions & 0 deletions lib/open_feature/sdk/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,22 @@ def remove_handler(event_type, handler = nil, &block)
OpenFeature::SDK.configuration.remove_client_handler(self, event_type, actual_handler)
end

# Tracking API (spec 6.1.1.1) — dynamic-context paradigm
#
# Records a tracking event. If the provider does not implement
# tracking, this is a no-op (spec 6.1.4).
def track(tracking_event_name, evaluation_context: nil, tracking_event_details: nil)
return unless @provider.respond_to?(:track)

built_context = EvaluationContextBuilder.new.call(
api_context: OpenFeature::SDK.evaluation_context,
client_context: self.evaluation_context,
invocation_context: evaluation_context
)

@provider.track(tracking_event_name, evaluation_context: built_context, tracking_event_details: tracking_event_details)
end

RESULT_TYPE.each do |result_type|
SUFFIXES.each do |suffix|
class_eval <<-RUBY, __FILE__, __LINE__ + 1
Expand Down
23 changes: 23 additions & 0 deletions lib/open_feature/sdk/tracking_event_details.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# frozen_string_literal: true

module OpenFeature
module SDK
# Represents tracking event details per spec section 6.2.
#
# Requirement 6.2.1: MUST define an optional numeric value.
# Requirement 6.2.2: MUST support custom fields (string keys,
# boolean/string/number/structure values).
class TrackingEventDetails
attr_reader :value, :fields

def initialize(value: nil, **fields)
if !value.nil? && !value.is_a?(Numeric)
raise ArgumentError, "Tracking event value must be Numeric, got #{value.class}"
end

@value = value
@fields = fields.transform_keys(&:to_s)
end
Comment on lines +13 to +20

Choose a reason for hiding this comment

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

medium

The OpenFeature specification requires the optional value in TrackingEventDetails to be numeric (spec 6.2.1). The current implementation allows any type for value. To ensure compliance and prevent potential issues with providers expecting a numeric type, I recommend adding a type check in the initializer. This will make the implementation more robust and prevent invalid data from being passed to providers.

      def initialize(value: nil, **fields)
        if !value.nil? && !value.is_a?(Numeric)
          raise ArgumentError, "Tracking event `value` must be a Numeric, but was #{value.class}"
        end

        @value = value
        @fields = fields.transform_keys(&:to_s)
      end

end
end
end
151 changes: 151 additions & 0 deletions spec/specification/tracking_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
# frozen_string_literal: true

require "spec_helper"

RSpec.describe "Tracking Specification" do
before(:each) do
OpenFeature::SDK::API.instance.send(:configuration).send(:reset)
end

context "6.1 - Tracking API" do
context "Condition 6.1.1.1" do
specify "The client MUST define a function for tracking with parameters: tracking event name (required), evaluation context (optional), and tracking event details (optional)" do
provider = OpenFeature::SDK::Provider::NoOpProvider.new
OpenFeature::SDK.set_provider(provider)
client = OpenFeature::SDK.build_client

expect(client).to respond_to(:track)

# Verify the method accepts the required and optional parameters
method = client.method(:track)
params = method.parameters

# First param is the required tracking event name
expect(params).to include([:req, :tracking_event_name])
end
end

context "Requirement 6.1.3" do
specify "The evaluation context passed to the provider's track function MUST be merged in the order: API → client → invocation" do
captured_context = nil

tracking_provider = Class.new do
def track(event_name, evaluation_context:, tracking_event_details:)
# Capture for assertion
end

def metadata
OpenFeature::SDK::Provider::ProviderMetadata.new(name: "tracking-provider")
end
end.new

allow(tracking_provider).to receive(:track) do |event_name, evaluation_context:, tracking_event_details:|
captured_context = evaluation_context
end

OpenFeature::SDK.configure do |config|
config.evaluation_context = OpenFeature::SDK::EvaluationContext.new(api_key: "api_value", shared: "api")
end
OpenFeature::SDK.set_provider(tracking_provider)

client = OpenFeature::SDK.build_client(
evaluation_context: OpenFeature::SDK::EvaluationContext.new(client_key: "client_value", shared: "client")
)

invocation_context = OpenFeature::SDK::EvaluationContext.new(
invocation_key: "invocation_value",
shared: "invocation"
)

client.track("checkout", evaluation_context: invocation_context)

expect(captured_context.field("api_key")).to eq("api_value")
expect(captured_context.field("client_key")).to eq("client_value")
expect(captured_context.field("invocation_key")).to eq("invocation_value")
# Invocation has highest precedence
expect(captured_context.field("shared")).to eq("invocation")
end
end

context "Requirement 6.1.4" do
specify "If the provider does not implement tracking, the client's track function MUST perform no operation" do
# NoOpProvider does not implement track
provider = OpenFeature::SDK::Provider::NoOpProvider.new
OpenFeature::SDK.set_provider(provider)
client = OpenFeature::SDK.build_client

expect { client.track("event-name") }.not_to raise_error
end

specify "If the provider implements tracking, the track function is called" do
track_called = false

tracking_provider = Class.new do
define_method(:track) do |event_name, evaluation_context:, tracking_event_details:|
track_called = true
end
end.new

OpenFeature::SDK.set_provider(tracking_provider)
client = OpenFeature::SDK.build_client

client.track("purchase")

expect(track_called).to be true
end
end
end

context "6.2 - Tracking Event Details" do
context "Requirement 6.2.1" do
specify "The tracking event details MUST define an optional numeric value" do
details = OpenFeature::SDK::TrackingEventDetails.new(value: 99.99)
expect(details.value).to eq(99.99)
end

specify "The value defaults to nil when not provided" do
details = OpenFeature::SDK::TrackingEventDetails.new
expect(details.value).to be_nil
end

specify "The value must be numeric if provided" do
expect { OpenFeature::SDK::TrackingEventDetails.new(value: "not_a_number") }.to raise_error(ArgumentError)
end
end

context "Requirement 6.2.2" do
specify "Tracking event details MUST support custom fields with string keys" do
details = OpenFeature::SDK::TrackingEventDetails.new(
value: 42,
item: "premium-plan",
quantity: 1,
enabled: true
)

expect(details.fields["item"]).to eq("premium-plan")
expect(details.fields["quantity"]).to eq(1)
expect(details.fields["enabled"]).to be true
end
end

specify "tracking event details are passed through to the provider" do
captured_details = nil

tracking_provider = Class.new do
define_method(:track) do |event_name, evaluation_context:, tracking_event_details:|
captured_details = tracking_event_details
end
end.new

OpenFeature::SDK.set_provider(tracking_provider)
client = OpenFeature::SDK.build_client

details = OpenFeature::SDK::TrackingEventDetails.new(value: 19.99, plan: "enterprise")
client.track("subscription", tracking_event_details: details)

expect(captured_details).to be_a(OpenFeature::SDK::TrackingEventDetails)
expect(captured_details.value).to eq(19.99)
expect(captured_details.fields["plan"]).to eq("enterprise")
end
end
end
Loading