@@ -23,7 +23,9 @@ import {
23
23
NamedCondition ,
24
24
OrCondition ,
25
25
PercentCondition ,
26
- PercentConditionOperator
26
+ PercentConditionOperator ,
27
+ CustomSignalCondition ,
28
+ CustomSignalOperator ,
27
29
} from './remote-config-api' ;
28
30
import * as farmhash from 'farmhash-modern' ;
29
31
@@ -76,6 +78,9 @@ export class ConditionEvaluator {
76
78
if ( condition . percent ) {
77
79
return this . evaluatePercentCondition ( condition . percent , context ) ;
78
80
}
81
+ if ( condition . customSignal ) {
82
+ return this . evaluateCustomSignalCondition ( condition . customSignal , context ) ;
83
+ }
79
84
// TODO: add logging once we have a wrapped logger.
80
85
return false ;
81
86
}
@@ -167,7 +172,6 @@ export class ConditionEvaluator {
167
172
return false ;
168
173
}
169
174
170
- // Visible for testing
171
175
static hashSeededRandomizationId ( seededRandomizationId : string ) : bigint {
172
176
// For consistency with the Remote Config fetch endpoint's percent condition behavior
173
177
// we use Farmhash's fingerprint64 algorithm and interpret the resulting unsigned value
@@ -182,4 +186,150 @@ export class ConditionEvaluator {
182
186
183
187
return hash64 ;
184
188
}
189
+
190
+ private evaluateCustomSignalCondition (
191
+ customSignalCondition : CustomSignalCondition ,
192
+ context : EvaluationContext
193
+ ) : boolean {
194
+ const {
195
+ customSignalOperator,
196
+ customSignalKey,
197
+ targetCustomSignalValues,
198
+ } = customSignalCondition ;
199
+
200
+ if ( ! customSignalOperator || ! customSignalKey || ! targetCustomSignalValues ) {
201
+ // TODO: add logging once we have a wrapped logger.
202
+ return false ;
203
+ }
204
+
205
+ if ( ! targetCustomSignalValues . length ) {
206
+ return false ;
207
+ }
208
+
209
+ // Extract the value of the signal from the evaluation context.
210
+ const actualCustomSignalValue = context [ customSignalKey ] ;
211
+
212
+ if ( actualCustomSignalValue == undefined ) {
213
+ return false
214
+ }
215
+
216
+ switch ( customSignalOperator ) {
217
+ case CustomSignalOperator . STRING_CONTAINS :
218
+ return compareStrings (
219
+ targetCustomSignalValues ,
220
+ actualCustomSignalValue ,
221
+ ( target , actual ) => actual . includes ( target ) ,
222
+ ) ;
223
+ case CustomSignalOperator . STRING_DOES_NOT_CONTAIN :
224
+ return ! compareStrings (
225
+ targetCustomSignalValues ,
226
+ actualCustomSignalValue ,
227
+ ( target , actual ) => actual . includes ( target ) ,
228
+ ) ;
229
+ case CustomSignalOperator . STRING_EXACTLY_MATCHES :
230
+ return compareStrings (
231
+ targetCustomSignalValues ,
232
+ actualCustomSignalValue ,
233
+ ( target , actual ) => actual . trim ( ) === target . trim ( ) ,
234
+ ) ;
235
+ case CustomSignalOperator . STRING_CONTAINS_REGEX :
236
+ return compareStrings (
237
+ targetCustomSignalValues ,
238
+ actualCustomSignalValue ,
239
+ ( target , actual ) => new RegExp ( target ) . test ( actual ) ,
240
+ ) ;
241
+
242
+ // For numeric operators only one target value is allowed.
243
+ case CustomSignalOperator . NUMERIC_LESS_THAN :
244
+ return compareNumbers ( actualCustomSignalValue , targetCustomSignalValues [ 0 ] , ( r ) => r < 0 ) ;
245
+ case CustomSignalOperator . NUMERIC_LESS_EQUAL :
246
+ return compareNumbers ( actualCustomSignalValue , targetCustomSignalValues [ 0 ] , ( r ) => r <= 0 ) ;
247
+ case CustomSignalOperator . NUMERIC_EQUAL :
248
+ return compareNumbers ( actualCustomSignalValue , targetCustomSignalValues [ 0 ] , ( r ) => r === 0 ) ;
249
+ case CustomSignalOperator . NUMERIC_NOT_EQUAL :
250
+ return compareNumbers ( actualCustomSignalValue , targetCustomSignalValues [ 0 ] , ( r ) => r !== 0 ) ;
251
+ case CustomSignalOperator . NUMERIC_GREATER_THAN :
252
+ return compareNumbers ( actualCustomSignalValue , targetCustomSignalValues [ 0 ] , ( r ) => r > 0 ) ;
253
+ case CustomSignalOperator . NUMERIC_GREATER_EQUAL :
254
+ return compareNumbers ( actualCustomSignalValue , targetCustomSignalValues [ 0 ] , ( r ) => r >= 0 ) ;
255
+
256
+ // For semantic operators only one target value is allowed.
257
+ case CustomSignalOperator . SEMANTIC_VERSION_LESS_THAN :
258
+ return compareSemanticVersions ( actualCustomSignalValue , targetCustomSignalValues [ 0 ] , ( r ) => r < 0 ) ;
259
+ case CustomSignalOperator . SEMANTIC_VERSION_LESS_EQUAL :
260
+ return compareSemanticVersions ( actualCustomSignalValue , targetCustomSignalValues [ 0 ] , ( r ) => r <= 0 ) ;
261
+ case CustomSignalOperator . SEMANTIC_VERSION_EQUAL :
262
+ return compareSemanticVersions ( actualCustomSignalValue , targetCustomSignalValues [ 0 ] , ( r ) => r === 0 ) ;
263
+ case CustomSignalOperator . SEMANTIC_VERSION_NOT_EQUAL :
264
+ return compareSemanticVersions ( actualCustomSignalValue , targetCustomSignalValues [ 0 ] , ( r ) => r !== 0 ) ;
265
+ case CustomSignalOperator . SEMANTIC_VERSION_GREATER_THAN :
266
+ return compareSemanticVersions ( actualCustomSignalValue , targetCustomSignalValues [ 0 ] , ( r ) => r > 0 ) ;
267
+ case CustomSignalOperator . SEMANTIC_VERSION_GREATER_EQUAL :
268
+ return compareSemanticVersions ( actualCustomSignalValue , targetCustomSignalValues [ 0 ] , ( r ) => r >= 0 ) ;
269
+ }
270
+
271
+ // TODO: add logging once we have a wrapped logger.
272
+ return false ;
273
+ }
274
+ }
275
+
276
+ // Compares the actual string value of a signal against a list of target
277
+ // values. If any of the target values are a match, returns true.
278
+ function compareStrings (
279
+ targetValues : Array < string > ,
280
+ actualValue : string | number ,
281
+ predicateFn : ( target : string , actual : string ) => boolean
282
+ ) : boolean {
283
+ const actual = String ( actualValue ) ;
284
+ return targetValues . some ( ( target ) => predicateFn ( target , actual ) ) ;
285
+ }
286
+
287
+ // Compares two numbers against each other.
288
+ // Calls the predicate function with -1, 0, 1 if actual is less than, equal to, or greater than target.
289
+ function compareNumbers (
290
+ actualValue : string | number ,
291
+ targetValue : string ,
292
+ predicateFn : ( result : number ) => boolean
293
+ ) : boolean {
294
+ const target = Number ( targetValue ) ;
295
+ const actual = Number ( actualValue ) ;
296
+ if ( isNaN ( target ) || isNaN ( actual ) ) {
297
+ return false ;
298
+ }
299
+ return predicateFn ( actual < target ? - 1 : actual > target ? 1 : 0 ) ;
300
+ }
301
+
302
+ // Max number of segments a numeric version can have. This is enforced by the server as well.
303
+ const MAX_LENGTH = 5 ;
304
+
305
+ // Compares semantic version strings against each other.
306
+ // Calls the predicate function with -1, 0, 1 if actual is less than, equal to, or greater than target.
307
+ function compareSemanticVersions (
308
+ actualValue : string | number ,
309
+ targetValue : string ,
310
+ predicateFn : ( result : number ) => boolean
311
+ ) : boolean {
312
+ const version1 = String ( actualValue ) . split ( '.' ) . map ( Number ) ;
313
+ const version2 = targetValue . split ( '.' ) . map ( Number ) ;
314
+
315
+ for ( let i = 0 ; i < MAX_LENGTH ; i ++ ) {
316
+ // Check to see if segments are present. Note that these may be present and be NaN.
317
+ const version1HasSegment = version1 [ i ] !== undefined ;
318
+ const version2HasSegment = version2 [ i ] !== undefined ;
319
+
320
+ // If both are undefined, we've consumed everything and they're equal.
321
+ if ( ! version1HasSegment && ! version2HasSegment ) return predicateFn ( 0 )
322
+
323
+ // Insert zeros if undefined for easier comparison.
324
+ if ( ! version1HasSegment ) version1 [ i ] = 0 ;
325
+ if ( ! version2HasSegment ) version2 [ i ] = 0 ;
326
+
327
+ // At this point, if either segment is NaN, we return false directly.
328
+ if ( isNaN ( version1 [ i ] ) || isNaN ( version2 [ i ] ) ) return false ;
329
+
330
+ // Check if we have a difference in segments. Otherwise continue to next segment.
331
+ if ( version1 [ i ] < version2 [ i ] ) return predicateFn ( - 1 ) ;
332
+ if ( version1 [ i ] > version2 [ i ] ) return predicateFn ( 1 ) ;
333
+ }
334
+ return false ;
185
335
}
0 commit comments