Skip to content

Commit 6f40c67

Browse files
authored
Merge pull request #6732 from Countly/sdk-exp-section
[sdk] Experimental section
2 parents cf0725a + 7163161 commit 6f40c67

File tree

7 files changed

+914
-111
lines changed

7 files changed

+914
-111
lines changed

bin/scripts/localization/clean-all-plugins.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ echo "Failed: $FAILED"
224224
echo "Plugins with unused keys: $WITH_UNUSED_KEYS"
225225
echo
226226

227-
if [ $FAILED -gt 0 ]; then
227+
if [ "$FAILED" -gt 0 ]; then
228228
echo "Failed plugins:"
229229
for plugin in "${FAILED_PLUGINS[@]}"; do
230230
echo " - $plugin"

plugins/sdk/api/api.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,16 @@ const validOptions = [
3232
"bom_at",
3333
"bom_rqp",
3434
"bom_ra",
35-
"bom_d"
35+
"bom_d",
36+
"upcl", // user property cache. dart only
37+
"ew", // event whitelist dart only
38+
"upw", // user property whitelist dart only
39+
"sw", // segment whitelist dart only
40+
"esw", // event segment whitelist dart only
41+
"eb", // event blacklist dart only
42+
"upb", // user property blacklist dart only
43+
"sb", // segment blacklist dart only
44+
"esb" // event segment blacklist dart only
3645
];
3746

3847
plugins.register("/permissions/features", function(ob) {

plugins/sdk/frontend/public/javascripts/countly.views.js

Lines changed: 434 additions & 7 deletions
Large diffs are not rendered by default.

plugins/sdk/frontend/public/stylesheets/main.scss

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,3 +110,61 @@
110110
border-color: #c6e2ff;
111111
}
112112
}
113+
114+
/* Some changes for making left column of config to stay top-aligned. */
115+
.bu-columns.config-section,
116+
.config-section.bu-columns,
117+
.config-section.bu-is-vcentered,
118+
.bu-columns.config-section.bu-is-vcentered {
119+
align-items: flex-start !important;
120+
}
121+
122+
/* SDK config validation styles */
123+
.config-invalid textarea,
124+
.config-invalid input,
125+
.config-invalid .el-textarea__inner {
126+
border-color: var(--cly-danger, #e74c3c) !important;
127+
box-shadow: 0 0 0 1px rgba(231, 76, 60, 0.12) !important;
128+
}
129+
130+
.validation-wrapper {
131+
display: flex;
132+
flex-direction: column;
133+
width: 100%;
134+
align-items: stretch;
135+
}
136+
137+
.config-validation-box {
138+
margin-top: 6px;
139+
font-size: 0.95em;
140+
width: 100%;
141+
box-sizing: border-box;
142+
}
143+
.config-validation-box .status-box {
144+
display: flex;
145+
align-items: center;
146+
gap: 8px;
147+
padding: 8px 12px;
148+
border-radius: 6px;
149+
border: 1px solid transparent;
150+
width: 100%;
151+
box-sizing: border-box;
152+
}
153+
.config-validation-box .status-box.valid {
154+
color: var(--cly-success, #176f2c);
155+
background: rgba(23, 111, 44, 0.06);
156+
border-color: rgba(23, 111, 44, 0.18);
157+
}
158+
.config-validation-box .status-box.invalid {
159+
color: var(--cly-danger, #c0392b);
160+
background: rgba(192, 57, 43, 0.04);
161+
border-color: rgba(192, 57, 43, 0.18);
162+
}
163+
.config-validation-box .dot {
164+
width: 10px;
165+
height: 10px;
166+
border-radius: 50%;
167+
display: inline-block;
168+
}
169+
.config-validation-box .dot.green { background: var(--cly-success, #2ecc71); }
170+
.config-validation-box .dot.red { background: var(--cly-danger, #e74c3c); }

plugins/sdk/frontend/public/templates/config.html

Lines changed: 148 additions & 102 deletions
Large diffs are not rendered by default.

plugins/sdk/tests/tests.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,19 @@ describe('SDK Plugin', function() {
171171
});
172172
});
173173

174+
it('1.1 uploads config via POST', function(done) {
175+
request
176+
.post('/o')
177+
.send({ method: 'config-upload', api_key: API_KEY_ADMIN, app_id: APP_ID, config: JSON.stringify({}) })
178+
.expect(200)
179+
.end(function(err, res) {
180+
should.not.exist(err);
181+
res.body.should.be.an.Object();
182+
res.body.should.have.property('result', 'Success');
183+
done();
184+
});
185+
});
186+
174187
checkBadCredentials('/o', 'config-upload');
175188

176189
it('7. should reject invalid config format', function(done) {
Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
const spt = require('supertest');
2+
const should = require('should');
3+
const testUtils = require('../../../test/testUtils');
4+
5+
const request = spt(testUtils.url);
6+
// change these in local testing directly or set env vars (also COUNTLY_CONFIG_HOSTNAME should be set with port)
7+
let API_KEY_ADMIN = testUtils.get("API_KEY_ADMIN");
8+
let APP_KEY = testUtils.get('APP_KEY');
9+
let APP_ID = testUtils.get("APP_ID");
10+
11+
describe('CSV/Array and JSON validation', function() {
12+
function unescapeHtml(str) {
13+
if (typeof str !== 'string') {
14+
return str;
15+
}
16+
return str.replace(/"/g, '"')
17+
.replace(/&/g, '&')
18+
.replace(/&lt;/g, '<')
19+
.replace(/&gt;/g, '>')
20+
.replace(/&#39;/g, "'");
21+
}
22+
before(function(done) {
23+
const enforcement = {
24+
eb: true,
25+
upb: true,
26+
sb: true,
27+
esb: true
28+
};
29+
30+
request
31+
.post('/i/sdk-config/update-enforcement')
32+
.query({ api_key: API_KEY_ADMIN, app_id: APP_ID, enforcement: JSON.stringify(enforcement) })
33+
.expect(200)
34+
.end(function(err, res) {
35+
should.not.exist(err);
36+
res.body.should.have.property('result', 'Success');
37+
done();
38+
});
39+
});
40+
41+
it('1. should save arrays for eb/upb/sb and objects for esb when provided as proper types', function(done) {
42+
const parameter = {
43+
eb: ['a', 'b, c', ' d '],
44+
upb: ['user_prop_1', 'user,prop,2'],
45+
sb: ['seg1', 'seg2'],
46+
esb: { 'event1': ['a', 'b'] }
47+
};
48+
49+
request
50+
.post('/i/sdk-config/update-parameter')
51+
.send({ api_key: API_KEY_ADMIN, app_id: APP_ID, parameter: JSON.stringify(parameter) })
52+
.expect(200)
53+
.end(function(err, res) {
54+
should.not.exist(err);
55+
res.body.should.have.property('result', 'Success');
56+
57+
request
58+
.get('/o/sdk')
59+
.query({ method: 'sc', app_key: APP_KEY, device_id: 'test' })
60+
.expect(200)
61+
.end(function(err, res) {
62+
should.not.exist(err);
63+
res.body.should.have.property('c');
64+
const c = res.body.c;
65+
c.should.have.property('eb');
66+
c.eb.should.be.an.Array();
67+
c.eb.should.have.length(3);
68+
c.eb.should.containEql('b, c');
69+
c.eb.should.containEql(' d ');
70+
c.eb.should.containEql('a');
71+
72+
c.should.have.property('upb');
73+
c.upb.should.be.an.Array();
74+
c.upb.should.containEql('user,prop,2');
75+
c.upb.should.containEql('user_prop_1');
76+
c.upb.should.have.length(2);
77+
78+
c.should.have.property('sb');
79+
c.sb.should.be.an.Array();
80+
c.sb.should.have.length(2);
81+
c.sb.should.containEql('seg1');
82+
c.sb.should.containEql('seg2');
83+
84+
c.should.have.property('esb');
85+
c.esb.should.be.an.Object();
86+
c.esb.should.have.property('event1');
87+
c.esb.event1.should.be.an.Array();
88+
c.esb.event1.should.have.length(2);
89+
c.esb.event1.should.containEql('a');
90+
c.esb.event1.should.containEql('b');
91+
done();
92+
});
93+
});
94+
});
95+
96+
// TODO: in future we may want to auto-parse CSV strings to arrays when uploaded, but for now front-end does this
97+
it('2. currently stores CSV strings as strings (server does not auto-parse CSV) and esb string stays string', function(done) {
98+
const parameter = {
99+
eb: 'one, "two, too", three',
100+
esb: 'this is not json'
101+
};
102+
103+
request
104+
.post('/i/sdk-config/update-parameter')
105+
.send({ api_key: API_KEY_ADMIN, app_id: APP_ID, parameter: JSON.stringify(parameter) })
106+
.expect(200)
107+
.end(function(err, res) {
108+
should.not.exist(err);
109+
res.body.should.have.property('result', 'Success');
110+
111+
request
112+
.get('/o/sdk')
113+
.query({ method: 'sc', app_key: APP_KEY, device_id: 'test' })
114+
.expect(200)
115+
.end(function(err, res) {
116+
should.not.exist(err);
117+
res.body.should.have.property('c');
118+
const c = res.body.c;
119+
c.should.have.property('eb');
120+
c.eb.should.be.a.String();
121+
unescapeHtml(c.eb).should.be.exactly('one, "two, too", three');
122+
123+
c.should.have.property('esb');
124+
c.esb.should.be.a.String();
125+
c.esb.should.be.exactly('this is not json');
126+
done();
127+
});
128+
});
129+
});
130+
131+
it('3. should reject invalid top-level parameter JSON (string) with 400', function(done) {
132+
request
133+
.post('/i/sdk-config/update-parameter')
134+
.send({ api_key: API_KEY_ADMIN, app_id: APP_ID, parameter: 'invalid json' })
135+
.expect(400)
136+
.end(function(err, res) {
137+
should.not.exist(err);
138+
res.body.should.have.property('result', 'Error parsing parameter');
139+
done();
140+
});
141+
});
142+
});
143+
144+
// CSV unit tests
145+
describe('CSV parse/serialize edge cases', function() {
146+
// copy pasta methods
147+
function csvToArray(str) {
148+
if (typeof str !== 'string') {
149+
return [];
150+
}
151+
return Array.from(str.matchAll(/(?:\s*("(?:[^"]|"")*"|[^,]*?)\s*)(?:,|$)/g)).map(function(m) {
152+
var val = m[1];
153+
if (!val) {
154+
return null;
155+
}
156+
if (val.charAt(0) === '"' && val.charAt(val.length - 1) === '"') {
157+
val = val.slice(1, -1).replace(/""/g, '"');
158+
}
159+
else {
160+
val = val.trim();
161+
}
162+
return val.length ? val : null;
163+
}).filter(function(v) {
164+
return v !== null;
165+
});
166+
}
167+
168+
function arrayToCsv(arr) {
169+
if (!Array.isArray(arr)) {
170+
return '';
171+
}
172+
return arr.filter(function(e) {
173+
return e != null;
174+
}).map(function(e) {
175+
e = String(e);
176+
// quote if contains comma, quote, newline or carriage return, or starts/ends with whitespace
177+
if (/[,"\n\r]/.test(e) || /^\s|\s$/.test(e)) {
178+
return '"' + e.replace(/"/g, '""') + '"';
179+
}
180+
return e;
181+
}).join(',');
182+
}
183+
184+
it('handles commas inside quoted fields', function() {
185+
const s = 'one,"two, too",three';
186+
csvToArray(s).should.eql(['one', 'two, too', 'three']);
187+
arrayToCsv(['one', 'two, too', 'three']).should.eql(s);
188+
});
189+
190+
it('handles escaped quotes inside quoted fields', function() {
191+
const s = 'a,"b""c",d';
192+
csvToArray(s).should.eql(['a', 'b"c', 'd']);
193+
arrayToCsv(['a', 'b"c', 'd']).should.eql(s);
194+
});
195+
196+
it('handles newlines inside quoted fields', function() {
197+
const s = '"line1\nline2",simple';
198+
csvToArray(s).should.eql(['line1\nline2', 'simple']);
199+
arrayToCsv(['line1\nline2', 'simple']).should.eql(s);
200+
});
201+
202+
it('drops empty fields', function() {
203+
const s = 'a,,b,,';
204+
csvToArray(s).should.eql(['a', 'b']);
205+
arrayToCsv(['a', 'b']).should.eql('a,b');
206+
});
207+
208+
it('arrayToCsv quotes fields when necessary and roundtrips correctly', function() {
209+
const arr = ['simple', 'has,comma', ' hasspace ', 'quotes"inside', 'multi\nline'];
210+
const csv = arrayToCsv(arr);
211+
csv.should.be.a.String();
212+
csv.should.eql('simple,"has,comma"," hasspace ","quotes""inside","multi\nline"');
213+
csvToArray(csv).should.eql(['simple', 'has,comma', ' hasspace ', 'quotes"inside', 'multi\nline']);
214+
});
215+
216+
it('arrayToCsv produces empty entries for null/undefined which arrayToCsv will drop', function() {
217+
const arr = ['a', null, undefined, 'b'];
218+
const csv = arrayToCsv(arr);
219+
csv.should.be.a.String();
220+
csv.should.eql('a,b');
221+
csvToArray(csv).should.eql(['a', 'b']);
222+
});
223+
224+
it('handles unicode characters correctly', function() {
225+
const arr = ['emoji 😊', 'accenté', '中文,文本'];
226+
const csv = arrayToCsv(arr);
227+
csv.should.be.a.String();
228+
csv.should.eql('emoji 😊,accenté,"中文,文本"');
229+
csvToArray(csv).should.eql(['emoji 😊', 'accenté', '中文,文本']);
230+
});
231+
232+
it('handles extremely long fields', function() {
233+
const long = 'x'.repeat(100000); // 100k chars
234+
const arr = ['start', long, 'end'];
235+
const csv = arrayToCsv(arr);
236+
csvToArray(csv).should.eql(['start', long, 'end']);
237+
}).timeout(5000);
238+
239+
it('handles carriage returns inside quoted fields (CR)', function() {
240+
const s = '"line1\rline2",after';
241+
csvToArray(s).should.eql(['line1\rline2', 'after']);
242+
arrayToCsv(['line1\rline2', 'after']).should.eql(s);
243+
});
244+
245+
it('handles CRLF inside quoted fields (CRLF)', function() {
246+
const s = '"a\r\nb",c';
247+
csvToArray(s).should.eql(['a\r\nb', 'c']);
248+
arrayToCsv(['a\r\nb', 'c']).should.eql(s);
249+
});
250+
});

0 commit comments

Comments
 (0)