Skip to content

Commit e6c269a

Browse files
fix: Update OpenFeature Provider user ID mapping priority and add userId support (#121)
* Support targeting_key, user_id, and userId with priority and custom data Co-authored-by: jonathan <jonathan@taplytics.com> * Checkpoint before follow-up message * Add user ID validation for OpenFeature context in DevCycle provider Co-authored-by: jonathan <jonathan@taplytics.com> * Update user ID validation error message with more descriptive guidance Co-authored-by: jonathan <jonathan@taplytics.com> * Validate user_id input with type check before string method call Co-authored-by: jonathan <jonathan@taplytics.com> * Refactor user ID validation to prevent NoMethodError on non-string types Co-authored-by: jonathan <jonathan@taplytics.com> * Improve user ID validation error message with field name Co-authored-by: jonathan <jonathan@taplytics.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com>
1 parent a963da8 commit e6c269a

File tree

2 files changed

+189
-12
lines changed

2 files changed

+189
-12
lines changed

lib/devcycle-ruby-server-sdk/api/dev_cycle_provider.rb

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,15 +48,42 @@ def self.user_from_openfeature_context(context)
4848
raise ArgumentError, "Invalid context type, expected OpenFeature::SDK::EvaluationContext but got #{context.class}"
4949
end
5050
args = {}
51-
if context.field('user_id')
52-
args.merge!(user_id: context.field('user_id'))
53-
elsif context.field('targeting_key')
54-
args.merge!(user_id: context.field('targeting_key'))
51+
user_id = nil
52+
user_id_field = nil
53+
54+
# Priority order: targeting_key -> user_id -> userId
55+
if context.field('targeting_key')
56+
user_id = context.field('targeting_key')
57+
user_id_field = 'targeting_key'
58+
elsif context.field('user_id')
59+
user_id = context.field('user_id')
60+
user_id_field = 'user_id'
61+
elsif context.field('userId')
62+
user_id = context.field('userId')
63+
user_id_field = 'userId'
5564
end
65+
66+
# Validate user_id is present and is a string
67+
if user_id.nil?
68+
raise ArgumentError, "User ID is required. Must provide one of: targeting_key, user_id, or userId"
69+
end
70+
71+
unless user_id.is_a?(String)
72+
raise ArgumentError, "User ID field '#{user_id_field}' must be a string, got #{user_id.class}"
73+
end
74+
75+
# Check after type validation to avoid NoMethodError on non-strings
76+
if user_id.empty?
77+
raise ArgumentError, "User ID is required. Must provide one of: targeting_key, user_id, or userId"
78+
end
79+
80+
args.merge!(user_id: user_id)
81+
5682
customData = {}
5783
privateCustomData = {}
5884
context.fields.each do |field, value|
59-
if field === 'user_id' || field === 'targeting_key'
85+
# Skip all user ID fields from custom data
86+
if field === 'targeting_key' || field === 'user_id' || field === 'userId'
6087
next
6188
end
6289
if !(field === 'privateCustomData' || field === 'customData') && value.is_a?(Hash)

spec/devcycle_provider_spec.rb

Lines changed: 157 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,109 @@
44
require 'open_feature/sdk'
55

66
context 'user_from_openfeature_context' do
7-
context 'user_id' do
7+
context 'user_id validation' do
8+
it 'raises error when no user ID fields are provided' do
9+
context = OpenFeature::SDK::EvaluationContext.new(email: 'test@example.com')
10+
expect {
11+
DevCycle::Provider.user_from_openfeature_context(context)
12+
}.to raise_error(ArgumentError, "User ID is required. Must provide one of: targeting_key, user_id, or userId")
13+
end
14+
15+
it 'raises error when targeting_key is not a string' do
16+
context = OpenFeature::SDK::EvaluationContext.new(targeting_key: 123)
17+
expect {
18+
DevCycle::Provider.user_from_openfeature_context(context)
19+
}.to raise_error(ArgumentError, "User ID field 'targeting_key' must be a string, got Integer")
20+
end
21+
22+
it 'raises error when user_id is not a string' do
23+
context = OpenFeature::SDK::EvaluationContext.new(user_id: 123)
24+
expect {
25+
DevCycle::Provider.user_from_openfeature_context(context)
26+
}.to raise_error(ArgumentError, "User ID field 'user_id' must be a string, got Integer")
27+
end
28+
29+
it 'raises error when userId is not a string' do
30+
context = OpenFeature::SDK::EvaluationContext.new(userId: 123)
31+
expect {
32+
DevCycle::Provider.user_from_openfeature_context(context)
33+
}.to raise_error(ArgumentError, "User ID field 'userId' must be a string, got Integer")
34+
end
35+
36+
it 'raises error when targeting_key is nil' do
37+
context = OpenFeature::SDK::EvaluationContext.new(targeting_key: nil)
38+
expect {
39+
DevCycle::Provider.user_from_openfeature_context(context)
40+
}.to raise_error(ArgumentError, "User ID is required. Must provide one of: targeting_key, user_id, or userId")
41+
end
842

9-
it 'returns a user with the user_id from the context' do
10-
context = OpenFeature::SDK::EvaluationContext.new(user_id: 'user_id')
43+
it 'raises error when targeting_key is empty string' do
44+
context = OpenFeature::SDK::EvaluationContext.new(targeting_key: '')
45+
expect {
46+
DevCycle::Provider.user_from_openfeature_context(context)
47+
}.to raise_error(ArgumentError, "User ID is required. Must provide one of: targeting_key, user_id, or userId")
48+
end
49+
50+
it 'raises error when user_id is empty string' do
51+
context = OpenFeature::SDK::EvaluationContext.new(user_id: '')
52+
expect {
53+
DevCycle::Provider.user_from_openfeature_context(context)
54+
}.to raise_error(ArgumentError, "User ID is required. Must provide one of: targeting_key, user_id, or userId")
55+
end
56+
57+
it 'raises error when userId is empty string' do
58+
context = OpenFeature::SDK::EvaluationContext.new(userId: '')
59+
expect {
60+
DevCycle::Provider.user_from_openfeature_context(context)
61+
}.to raise_error(ArgumentError, "User ID is required. Must provide one of: targeting_key, user_id, or userId")
62+
end
63+
end
64+
65+
context 'user_id fields priority' do
66+
it 'returns a user with the user_id from the context when only user_id is provided' do
67+
context = OpenFeature::SDK::EvaluationContext.new(user_id: 'user_id_value')
1168
user = DevCycle::Provider.user_from_openfeature_context(context)
12-
expect(user.user_id).to eq('user_id')
69+
expect(user.user_id).to eq('user_id_value')
1370
end
1471

15-
it 'returns a user with the targeting_key from the context' do
16-
context = OpenFeature::SDK::EvaluationContext.new(targeting_key: 'targeting_key')
72+
it 'returns a user with the targeting_key from the context when only targeting_key is provided' do
73+
context = OpenFeature::SDK::EvaluationContext.new(targeting_key: 'targeting_key_value')
1774
user = DevCycle::Provider.user_from_openfeature_context(context)
18-
expect(user.user_id).to eq('targeting_key')
75+
expect(user.user_id).to eq('targeting_key_value')
76+
end
77+
78+
it 'returns a user with the userId from the context when only userId is provided' do
79+
context = OpenFeature::SDK::EvaluationContext.new(userId: 'userId_value')
80+
user = DevCycle::Provider.user_from_openfeature_context(context)
81+
expect(user.user_id).to eq('userId_value')
82+
end
83+
84+
it 'prioritizes targeting_key over user_id' do
85+
context = OpenFeature::SDK::EvaluationContext.new(targeting_key: 'targeting_key_value', user_id: 'user_id_value')
86+
user = DevCycle::Provider.user_from_openfeature_context(context)
87+
expect(user.user_id).to eq('targeting_key_value')
88+
expect(user.customData).to eq({})
89+
end
90+
91+
it 'prioritizes targeting_key over userId' do
92+
context = OpenFeature::SDK::EvaluationContext.new(targeting_key: 'targeting_key_value', userId: 'userId_value')
93+
user = DevCycle::Provider.user_from_openfeature_context(context)
94+
expect(user.user_id).to eq('targeting_key_value')
95+
expect(user.customData).to eq({})
96+
end
97+
98+
it 'prioritizes user_id over userId' do
99+
context = OpenFeature::SDK::EvaluationContext.new(user_id: 'user_id_value', userId: 'userId_value')
100+
user = DevCycle::Provider.user_from_openfeature_context(context)
101+
expect(user.user_id).to eq('user_id_value')
102+
expect(user.customData).to eq({})
103+
end
104+
105+
it 'prioritizes targeting_key over both user_id and userId' do
106+
context = OpenFeature::SDK::EvaluationContext.new(targeting_key: 'targeting_key_value', user_id: 'user_id_value', userId: 'userId_value')
107+
user = DevCycle::Provider.user_from_openfeature_context(context)
108+
expect(user.user_id).to eq('targeting_key_value')
109+
expect(user.customData).to eq({})
19110
end
20111
end
21112
context 'email' do
@@ -31,6 +122,19 @@
31122
expect(user.user_id).to eq('targeting_key')
32123
expect(user.email).to eq('email')
33124
end
125+
it 'returns a user with a valid userId and email' do
126+
context = OpenFeature::SDK::EvaluationContext.new(userId: 'userId', email: 'email')
127+
user = DevCycle::Provider.user_from_openfeature_context(context)
128+
expect(user.user_id).to eq('userId')
129+
expect(user.email).to eq('email')
130+
end
131+
it 'prioritizes targeting_key over user_id with email' do
132+
context = OpenFeature::SDK::EvaluationContext.new(targeting_key: 'targeting_key', user_id: 'user_id', email: 'email')
133+
user = DevCycle::Provider.user_from_openfeature_context(context)
134+
expect(user.user_id).to eq('targeting_key')
135+
expect(user.email).to eq('email')
136+
expect(user.customData).to eq({})
137+
end
34138
end
35139

36140
context 'customData' do
@@ -40,6 +144,18 @@
40144
expect(user.user_id).to eq('user_id')
41145
expect(user.customData).to eq({ 'key' => 'value' })
42146
end
147+
it 'returns a user with userId and customData' do
148+
context = OpenFeature::SDK::EvaluationContext.new(userId: 'userId', customData: { 'key' => 'value' })
149+
user = DevCycle::Provider.user_from_openfeature_context(context)
150+
expect(user.user_id).to eq('userId')
151+
expect(user.customData).to eq({ 'key' => 'value' })
152+
end
153+
it 'excludes all user ID fields from customData' do
154+
context = OpenFeature::SDK::EvaluationContext.new(targeting_key: 'targeting_key', user_id: 'user_id', userId: 'userId', customData: { 'key' => 'value' })
155+
user = DevCycle::Provider.user_from_openfeature_context(context)
156+
expect(user.user_id).to eq('targeting_key')
157+
expect(user.customData).to eq({ 'key' => 'value' })
158+
end
43159
end
44160

45161
context 'privateCustomData' do
@@ -49,6 +165,12 @@
49165
expect(user.user_id).to eq('user_id')
50166
expect(user.privateCustomData).to eq({ 'key' => 'value' })
51167
end
168+
it 'returns a user with userId and privateCustomData' do
169+
context = OpenFeature::SDK::EvaluationContext.new(userId: 'userId', privateCustomData: { 'key' => 'value' })
170+
user = DevCycle::Provider.user_from_openfeature_context(context)
171+
expect(user.user_id).to eq('userId')
172+
expect(user.privateCustomData).to eq({ 'key' => 'value' })
173+
end
52174
end
53175

54176
context 'appVersion' do
@@ -65,6 +187,20 @@
65187
expect(user.user_id).to eq('user_id')
66188
expect(user.appBuild).to eq(1)
67189
end
190+
191+
it 'returns a user with userId and appVersion' do
192+
context = OpenFeature::SDK::EvaluationContext.new(userId: 'userId', appVersion: '1.0.0')
193+
user = DevCycle::Provider.user_from_openfeature_context(context)
194+
expect(user.user_id).to eq('userId')
195+
expect(user.appVersion).to eq('1.0.0')
196+
end
197+
198+
it 'returns a user with userId and appBuild' do
199+
context = OpenFeature::SDK::EvaluationContext.new(userId: 'userId', appBuild: 1)
200+
user = DevCycle::Provider.user_from_openfeature_context(context)
201+
expect(user.user_id).to eq('userId')
202+
expect(user.appBuild).to eq(1)
203+
end
68204
end
69205
context 'randomFields' do
70206
it 'returns a user with customData fields mapped to any non-standard fields' do
@@ -73,6 +209,20 @@
73209
expect(user.user_id).to eq('user_id')
74210
expect(user.customData).to eq({ 'randomField' => 'value' })
75211
end
212+
213+
it 'returns a user with userId and customData fields mapped to any non-standard fields' do
214+
context = OpenFeature::SDK::EvaluationContext.new(userId: 'userId', randomField: 'value')
215+
user = DevCycle::Provider.user_from_openfeature_context(context)
216+
expect(user.user_id).to eq('userId')
217+
expect(user.customData).to eq({ 'randomField' => 'value' })
218+
end
219+
220+
it 'excludes all user ID fields from custom data with random fields' do
221+
context = OpenFeature::SDK::EvaluationContext.new(targeting_key: 'targeting_key', user_id: 'user_id', userId: 'userId', randomField: 'value')
222+
user = DevCycle::Provider.user_from_openfeature_context(context)
223+
expect(user.user_id).to eq('targeting_key')
224+
expect(user.customData).to eq({ 'randomField' => 'value' })
225+
end
76226
end
77227

78228
context 'provider' do

0 commit comments

Comments
 (0)