Skip to content

Commit a8e51fb

Browse files
Merge pull request #160 from splitio/FME-11223-web-support-client-evaluation
[Web support] Connect factory `client` and client `getTreatment` methods
2 parents 3518c16 + c420659 commit a8e51fb

File tree

3 files changed

+280
-8
lines changed

3 files changed

+280
-8
lines changed

splitio_web/lib/splitio_web.dart

Lines changed: 103 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ class SplitioWeb extends SplitioPlatform {
2525
String? _trafficType;
2626
bool _impressionListener = false;
2727

28+
final Map<String, JS_IBrowserClient> _clients = {};
29+
2830
@override
2931
Future<void> init({
3032
required String apiKey,
@@ -107,7 +109,8 @@ class SplitioWeb extends SplitioPlatform {
107109
}.toJS;
108110

109111
script.onerror = (Event event) {
110-
completer.completeError(Exception('Failed to load Split SDK'));
112+
completer.completeError(
113+
Exception('Failed to load Split SDK, with error: $event'));
111114
}.toJS;
112115

113116
document.head!.appendChild(script);
@@ -311,4 +314,103 @@ class SplitioWeb extends SplitioPlatform {
311314
}
312315
return matchingKey.toJS;
313316
}
317+
318+
static String _buildKeyString(String matchingKey, String? bucketingKey) {
319+
return bucketingKey == null ? matchingKey : '${matchingKey}_$bucketingKey';
320+
}
321+
322+
@override
323+
Future<void> getClient({
324+
required String matchingKey,
325+
required String? bucketingKey,
326+
}) async {
327+
await this._initFuture;
328+
329+
final key = _buildKeyString(matchingKey, bucketingKey);
330+
331+
if (_clients.containsKey(key)) {
332+
return;
333+
}
334+
335+
final client = this._factory.client.callAsFunction(
336+
null, _buildKey(matchingKey, bucketingKey)) as JS_IBrowserClient;
337+
338+
_clients[key] = client;
339+
}
340+
341+
Future<JS_IBrowserClient> _getClient({
342+
required String matchingKey,
343+
required String? bucketingKey,
344+
}) async {
345+
await getClient(matchingKey: matchingKey, bucketingKey: bucketingKey);
346+
347+
final key = _buildKeyString(matchingKey, bucketingKey);
348+
349+
return _clients[key]!;
350+
}
351+
352+
JSAny? _convertValue(dynamic value, bool isAttributes) {
353+
if (value is bool) return value.toJS;
354+
if (value is num) return value.toJS; // covers int + double
355+
if (value is String) return value.toJS;
356+
357+
// properties do not support lists and sets
358+
if (isAttributes) {
359+
if (value is List) return value.jsify();
360+
if (value is Set) return value.jsify();
361+
}
362+
363+
return null;
364+
}
365+
366+
JSObject _convertMap(Map<String, dynamic> dartMap, bool isAttributes) {
367+
final jsMap = JSObject();
368+
369+
dartMap.forEach((key, value) {
370+
final jsValue = _convertValue(value, isAttributes);
371+
372+
if (jsValue != null) {
373+
jsMap.setProperty(key.toJS, jsValue);
374+
} else {
375+
this._factory.settings.log.warn.callAsFunction(
376+
null,
377+
'Invalid ${isAttributes ? 'attribute' : 'property'} value: $value, for key: $key, will be ignored'
378+
.toJS);
379+
}
380+
});
381+
382+
return jsMap;
383+
}
384+
385+
JSObject _convertEvaluationOptions(EvaluationOptions evaluationOptions) {
386+
final jsEvalOptions = JSObject();
387+
388+
if (evaluationOptions.properties.isNotEmpty) {
389+
jsEvalOptions.setProperty(
390+
'properties'.toJS, _convertMap(evaluationOptions.properties, false));
391+
}
392+
393+
return jsEvalOptions;
394+
}
395+
396+
@override
397+
Future<String> getTreatment({
398+
required String matchingKey,
399+
required String? bucketingKey,
400+
required String splitName,
401+
Map<String, dynamic> attributes = const {},
402+
EvaluationOptions evaluationOptions = const EvaluationOptions.empty(),
403+
}) async {
404+
final client = await _getClient(
405+
matchingKey: matchingKey,
406+
bucketingKey: bucketingKey,
407+
);
408+
409+
final result = client.getTreatment.callAsFunction(
410+
null,
411+
splitName.toJS,
412+
_convertMap(attributes, true),
413+
_convertEvaluationOptions(evaluationOptions)) as JSString;
414+
return result.toDart;
415+
}
314416
}

splitio_web/lib/src/js_interop.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,14 @@ extension type JS_ISettings._(JSObject _) implements JSObject {
1212
external JS_Logger log;
1313
}
1414

15+
@JS()
16+
extension type JS_IBrowserClient._(JSObject _) implements JSObject {
17+
external JSFunction getTreatment;
18+
}
19+
1520
@JS()
1621
extension type JS_IBrowserSDK._(JSObject _) implements JSObject {
22+
external JSFunction client;
1723
external JS_ISettings settings;
1824
}
1925

splitio_web/test/splitio_web_test.dart

Lines changed: 171 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'package:splitio_web/splitio_web.dart';
66
import 'package:splitio_web/src/js_interop.dart';
77
import 'package:splitio_platform_interface/split_certificate_pinning_configuration.dart';
88
import 'package:splitio_platform_interface/split_configuration.dart';
9+
import 'package:splitio_platform_interface/split_evaluation_options.dart';
910
import 'package:splitio_platform_interface/split_sync_config.dart';
1011
import 'package:splitio_platform_interface/split_rollout_cache_configuration.dart';
1112

@@ -15,26 +16,160 @@ extension on web.Window {
1516
}
1617

1718
void main() {
18-
final List<({String methodName, List<dynamic> methodArguments})> calls = [];
19+
final List<({String methodName, List<JSAny?> methodArguments})> calls = [];
20+
21+
final mockClient = JSObject();
22+
mockClient['getTreatment'] =
23+
(JSAny? flagName, JSAny? attributes, JSAny? evaluationOptions) {
24+
calls.add((
25+
methodName: 'getTreatment',
26+
methodArguments: [flagName, attributes, evaluationOptions]
27+
));
28+
return 'on'.toJS;
29+
}.toJS;
1930

2031
final mockLog = JSObject();
2132
mockLog['warn'] = (JSAny? arg1) {
2233
calls.add((methodName: 'warn', methodArguments: [arg1]));
2334
}.toJS;
35+
2436
final mockSettings = JSObject();
2537
mockSettings['log'] = mockLog;
2638

2739
final mockFactory = JSObject();
2840
mockFactory['settings'] = mockSettings;
41+
mockFactory['client'] = (JSAny? splitKey) {
42+
calls.add((methodName: 'client', methodArguments: [splitKey]));
43+
return mockClient;
44+
}.toJS;
2945

3046
final mockSplitio = JSObject();
3147
mockSplitio['SplitFactory'] = (JSAny? arg1) {
3248
calls.add((methodName: 'SplitFactory', methodArguments: [arg1]));
3349
return mockFactory;
3450
}.toJS;
3551

52+
SplitioWeb _platform = SplitioWeb();
53+
3654
setUp(() {
37-
(web.window as JSObject).setProperty('splitio'.toJS, mockSplitio);
55+
(web.window as JSObject)['splitio'] = mockSplitio;
56+
57+
_platform.init(
58+
apiKey: 'apiKey',
59+
matchingKey: 'matching-key',
60+
bucketingKey: 'bucketing-key');
61+
});
62+
63+
group('evaluation', () {
64+
test('getTreatment', () async {
65+
final result = await _platform.getTreatment(
66+
matchingKey: 'matching-key',
67+
bucketingKey: 'bucketing-key',
68+
splitName: 'split');
69+
70+
expect(result, 'on');
71+
expect(calls.last.methodName, 'getTreatment');
72+
expect(calls.last.methodArguments.map(jsAnyToDart), ['split', {}, {}]);
73+
});
74+
75+
test('getTreatment with attributes', () async {
76+
final result = await _platform.getTreatment(
77+
matchingKey: 'matching-key',
78+
bucketingKey: 'bucketing-key',
79+
splitName: 'split',
80+
attributes: {
81+
'attrBool': true,
82+
'attrString': 'value',
83+
'attrInt': 1,
84+
'attrDouble': 1.1,
85+
'attrList': ['value1', 100, false],
86+
'attrSet': {'value3', 100, true},
87+
'attrNull': null, // not valid attribute value
88+
'attrMap': {'value5': true} // not valid attribute value
89+
});
90+
91+
expect(result, 'on');
92+
expect(calls.last.methodName, 'getTreatment');
93+
expect(calls.last.methodArguments.map(jsAnyToDart), [
94+
'split',
95+
{
96+
'attrBool': true,
97+
'attrString': 'value',
98+
'attrInt': 1,
99+
'attrDouble': 1.1,
100+
'attrList': ['value1', 100, false],
101+
'attrSet': ['value3', 100, true]
102+
},
103+
{}
104+
]);
105+
106+
// assert warnings
107+
expect(calls[calls.length - 2].methodName, 'warn');
108+
expect(
109+
jsAnyToDart(calls[calls.length - 2].methodArguments[0]),
110+
equals(
111+
'Invalid attribute value: {value5: true}, for key: attrMap, will be ignored'));
112+
expect(calls[calls.length - 3].methodName, 'warn');
113+
expect(
114+
jsAnyToDart(calls[calls.length - 3].methodArguments[0]),
115+
equals(
116+
'Invalid attribute value: null, for key: attrNull, will be ignored'));
117+
});
118+
119+
test('getTreatment with evaluation properties', () async {
120+
final result = await _platform.getTreatment(
121+
matchingKey: 'matching-key',
122+
bucketingKey: 'bucketing-key',
123+
splitName: 'split',
124+
evaluationOptions: EvaluationOptions({
125+
'propBool': true,
126+
'propString': 'value',
127+
'propInt': 1,
128+
'propDouble': 1.1,
129+
'propList': ['value1', 100, false], // not valid property value
130+
'propSet': {'value3', 100, true}, // not valid property value
131+
'propNull': null, // not valid property value
132+
'propMap': {'value5': true} // not valid property value
133+
}));
134+
135+
expect(result, 'on');
136+
expect(calls.last.methodName, 'getTreatment');
137+
expect(calls.last.methodArguments.map(jsAnyToDart), [
138+
'split',
139+
{},
140+
{
141+
'properties': {
142+
'propBool': true,
143+
'propString': 'value',
144+
'propInt': 1,
145+
'propDouble': 1.1,
146+
}
147+
}
148+
]);
149+
150+
// assert warnings
151+
expect(calls[calls.length - 2].methodName, 'warn');
152+
expect(
153+
jsAnyToDart(calls[calls.length - 2].methodArguments[0]),
154+
equals(
155+
'Invalid property value: {value5: true}, for key: propMap, will be ignored'));
156+
expect(calls[calls.length - 3].methodName, 'warn');
157+
expect(
158+
jsAnyToDart(calls[calls.length - 3].methodArguments[0]),
159+
equals(
160+
'Invalid property value: null, for key: propNull, will be ignored'));
161+
expect(calls[calls.length - 4].methodName, 'warn');
162+
expect(
163+
jsAnyToDart(calls[calls.length - 4].methodArguments[0]),
164+
equals(
165+
'Invalid property value: {value3, 100, true}, for key: propSet, will be ignored'));
166+
expect(calls[calls.length - 5].methodName, 'warn');
167+
expect(
168+
jsAnyToDart(calls[calls.length - 5].methodArguments[0]),
169+
equals(
170+
'Invalid property value: [value1, 100, false], for key: propList, will be ignored'));
171+
});
172+
38173
});
39174

40175
group('initialization', () {
@@ -46,7 +181,7 @@ void main() {
46181

47182
expect(calls.last.methodName, 'SplitFactory');
48183
expect(
49-
jsObjectToMap(calls.last.methodArguments[0]),
184+
jsAnyToDart(calls.last.methodArguments[0]),
50185
equals({
51186
'core': {
52187
'authorizationKey': 'api-key',
@@ -65,7 +200,7 @@ void main() {
65200

66201
expect(calls.last.methodName, 'SplitFactory');
67202
expect(
68-
jsObjectToMap(calls.last.methodArguments[0]),
203+
jsAnyToDart(calls.last.methodArguments[0]),
69204
equals({
70205
'core': {
71206
'authorizationKey': 'api-key',
@@ -88,7 +223,7 @@ void main() {
88223

89224
expect(calls.last.methodName, 'SplitFactory');
90225
expect(
91-
jsObjectToMap(calls.last.methodArguments[0]),
226+
jsAnyToDart(calls.last.methodArguments[0]),
92227
equals({
93228
'core': {
94229
'authorizationKey': 'api-key',
@@ -151,7 +286,7 @@ void main() {
151286

152287
expect(calls[calls.length - 5].methodName, 'SplitFactory');
153288
expect(
154-
jsObjectToMap(calls[calls.length - 5].methodArguments[0]),
289+
jsAnyToDart(calls[calls.length - 5].methodArguments[0]),
155290
equals({
156291
'core': {
157292
'authorizationKey': 'api-key',
@@ -240,7 +375,7 @@ void main() {
240375

241376
expect(calls.last.methodName, 'SplitFactory');
242377
expect(
243-
jsObjectToMap(calls.last.methodArguments[0]),
378+
jsAnyToDart(calls.last.methodArguments[0]),
244379
equals({
245380
'core': {
246381
'authorizationKey': 'api-key',
@@ -266,4 +401,33 @@ void main() {
266401
}));
267402
});
268403
});
404+
405+
group('client', () {
406+
test('get client with no keys', () async {
407+
await _platform.getClient(
408+
matchingKey: 'matching-key', bucketingKey: null);
409+
410+
expect(calls.last.methodName, 'client');
411+
expect(calls.last.methodArguments.map(jsAnyToDart), ['matching-key']);
412+
});
413+
414+
test('get client with new matching key', () async {
415+
await _platform.getClient(
416+
matchingKey: 'new-matching-key', bucketingKey: null);
417+
418+
expect(calls.last.methodName, 'client');
419+
expect(calls.last.methodArguments.map(jsAnyToDart), ['new-matching-key']);
420+
});
421+
422+
test('get client with new matching key and bucketing key', () async {
423+
await _platform.getClient(
424+
matchingKey: 'new-matching-key', bucketingKey: 'bucketing-key');
425+
426+
expect(calls.last.methodName, 'client');
427+
expect(calls.last.methodArguments.map(jsAnyToDart), [
428+
{'matchingKey': 'new-matching-key', 'bucketingKey': 'bucketing-key'}
429+
]);
430+
});
431+
});
432+
269433
}

0 commit comments

Comments
 (0)