Skip to content

Commit 5576fce

Browse files
josecolellaclaude
andauthored
feat: implement Tracking API (spec section 6) (#227)
Signed-off-by: Jose Colella <jose.colella@gusto.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent a185003 commit 5576fce

4 files changed

Lines changed: 191 additions & 0 deletions

File tree

lib/open_feature/sdk/api.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
require_relative "client_metadata"
1111
require_relative "hooks"
1212
require_relative "client"
13+
require_relative "tracking_event_details"
1314
require_relative "provider"
1415

1516
module OpenFeature

lib/open_feature/sdk/client.rb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,22 @@ def remove_handler(event_type, handler = nil, &block)
4242
OpenFeature::SDK.configuration.remove_client_handler(self, event_type, actual_handler)
4343
end
4444

45+
# Tracking API (spec 6.1.1.1) — dynamic-context paradigm
46+
#
47+
# Records a tracking event. If the provider does not implement
48+
# tracking, this is a no-op (spec 6.1.4).
49+
def track(tracking_event_name, evaluation_context: nil, tracking_event_details: nil)
50+
return unless @provider.respond_to?(:track)
51+
52+
built_context = EvaluationContextBuilder.new.call(
53+
api_context: OpenFeature::SDK.evaluation_context,
54+
client_context: self.evaluation_context,
55+
invocation_context: evaluation_context
56+
)
57+
58+
@provider.track(tracking_event_name, evaluation_context: built_context, tracking_event_details: tracking_event_details)
59+
end
60+
4561
RESULT_TYPE.each do |result_type|
4662
SUFFIXES.each do |suffix|
4763
class_eval <<-RUBY, __FILE__, __LINE__ + 1
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# frozen_string_literal: true
2+
3+
module OpenFeature
4+
module SDK
5+
# Represents tracking event details per spec section 6.2.
6+
#
7+
# Requirement 6.2.1: MUST define an optional numeric value.
8+
# Requirement 6.2.2: MUST support custom fields (string keys,
9+
# boolean/string/number/structure values).
10+
class TrackingEventDetails
11+
attr_reader :value, :fields
12+
13+
def initialize(value: nil, **fields)
14+
if !value.nil? && !value.is_a?(Numeric)
15+
raise ArgumentError, "Tracking event value must be Numeric, got #{value.class}"
16+
end
17+
18+
@value = value
19+
@fields = fields.transform_keys(&:to_s)
20+
end
21+
end
22+
end
23+
end
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
# frozen_string_literal: true
2+
3+
require "spec_helper"
4+
5+
RSpec.describe "Tracking Specification" do
6+
before(:each) do
7+
OpenFeature::SDK::API.instance.send(:configuration).send(:reset)
8+
end
9+
10+
context "6.1 - Tracking API" do
11+
context "Condition 6.1.1.1" do
12+
specify "The client MUST define a function for tracking with parameters: tracking event name (required), evaluation context (optional), and tracking event details (optional)" do
13+
provider = OpenFeature::SDK::Provider::NoOpProvider.new
14+
OpenFeature::SDK.set_provider(provider)
15+
client = OpenFeature::SDK.build_client
16+
17+
expect(client).to respond_to(:track)
18+
19+
# Verify the method accepts the required and optional parameters
20+
method = client.method(:track)
21+
params = method.parameters
22+
23+
# First param is the required tracking event name
24+
expect(params).to include([:req, :tracking_event_name])
25+
end
26+
end
27+
28+
context "Requirement 6.1.3" do
29+
specify "The evaluation context passed to the provider's track function MUST be merged in the order: API → client → invocation" do
30+
captured_context = nil
31+
32+
tracking_provider = Class.new do
33+
def track(event_name, evaluation_context:, tracking_event_details:)
34+
# Capture for assertion
35+
end
36+
37+
def metadata
38+
OpenFeature::SDK::Provider::ProviderMetadata.new(name: "tracking-provider")
39+
end
40+
end.new
41+
42+
allow(tracking_provider).to receive(:track) do |event_name, evaluation_context:, tracking_event_details:|
43+
captured_context = evaluation_context
44+
end
45+
46+
OpenFeature::SDK.configure do |config|
47+
config.evaluation_context = OpenFeature::SDK::EvaluationContext.new(api_key: "api_value", shared: "api")
48+
end
49+
OpenFeature::SDK.set_provider(tracking_provider)
50+
51+
client = OpenFeature::SDK.build_client(
52+
evaluation_context: OpenFeature::SDK::EvaluationContext.new(client_key: "client_value", shared: "client")
53+
)
54+
55+
invocation_context = OpenFeature::SDK::EvaluationContext.new(
56+
invocation_key: "invocation_value",
57+
shared: "invocation"
58+
)
59+
60+
client.track("checkout", evaluation_context: invocation_context)
61+
62+
expect(captured_context.field("api_key")).to eq("api_value")
63+
expect(captured_context.field("client_key")).to eq("client_value")
64+
expect(captured_context.field("invocation_key")).to eq("invocation_value")
65+
# Invocation has highest precedence
66+
expect(captured_context.field("shared")).to eq("invocation")
67+
end
68+
end
69+
70+
context "Requirement 6.1.4" do
71+
specify "If the provider does not implement tracking, the client's track function MUST perform no operation" do
72+
# NoOpProvider does not implement track
73+
provider = OpenFeature::SDK::Provider::NoOpProvider.new
74+
OpenFeature::SDK.set_provider(provider)
75+
client = OpenFeature::SDK.build_client
76+
77+
expect { client.track("event-name") }.not_to raise_error
78+
end
79+
80+
specify "If the provider implements tracking, the track function is called" do
81+
track_called = false
82+
83+
tracking_provider = Class.new do
84+
define_method(:track) do |event_name, evaluation_context:, tracking_event_details:|
85+
track_called = true
86+
end
87+
end.new
88+
89+
OpenFeature::SDK.set_provider(tracking_provider)
90+
client = OpenFeature::SDK.build_client
91+
92+
client.track("purchase")
93+
94+
expect(track_called).to be true
95+
end
96+
end
97+
end
98+
99+
context "6.2 - Tracking Event Details" do
100+
context "Requirement 6.2.1" do
101+
specify "The tracking event details MUST define an optional numeric value" do
102+
details = OpenFeature::SDK::TrackingEventDetails.new(value: 99.99)
103+
expect(details.value).to eq(99.99)
104+
end
105+
106+
specify "The value defaults to nil when not provided" do
107+
details = OpenFeature::SDK::TrackingEventDetails.new
108+
expect(details.value).to be_nil
109+
end
110+
111+
specify "The value must be numeric if provided" do
112+
expect { OpenFeature::SDK::TrackingEventDetails.new(value: "not_a_number") }.to raise_error(ArgumentError)
113+
end
114+
end
115+
116+
context "Requirement 6.2.2" do
117+
specify "Tracking event details MUST support custom fields with string keys" do
118+
details = OpenFeature::SDK::TrackingEventDetails.new(
119+
value: 42,
120+
item: "premium-plan",
121+
quantity: 1,
122+
enabled: true
123+
)
124+
125+
expect(details.fields["item"]).to eq("premium-plan")
126+
expect(details.fields["quantity"]).to eq(1)
127+
expect(details.fields["enabled"]).to be true
128+
end
129+
end
130+
131+
specify "tracking event details are passed through to the provider" do
132+
captured_details = nil
133+
134+
tracking_provider = Class.new do
135+
define_method(:track) do |event_name, evaluation_context:, tracking_event_details:|
136+
captured_details = tracking_event_details
137+
end
138+
end.new
139+
140+
OpenFeature::SDK.set_provider(tracking_provider)
141+
client = OpenFeature::SDK.build_client
142+
143+
details = OpenFeature::SDK::TrackingEventDetails.new(value: 19.99, plan: "enterprise")
144+
client.track("subscription", tracking_event_details: details)
145+
146+
expect(captured_details).to be_a(OpenFeature::SDK::TrackingEventDetails)
147+
expect(captured_details.value).to eq(19.99)
148+
expect(captured_details.fields["plan"]).to eq("enterprise")
149+
end
150+
end
151+
end

0 commit comments

Comments
 (0)