Skip to content

Commit bb82231

Browse files
committed
Merge branch 'release.24.10' into release.24.12
# Conflicts: # CHANGELOG.md # plugins/hooks/frontend/public/localization/hooks.properties
2 parents 4eb96ad + f95529b commit bb82231

File tree

10 files changed

+403
-7
lines changed

10 files changed

+403
-7
lines changed

CHANGELOG.md

+13-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
## Version 24.12
1+
## Version 25.03
22
Features:
33
- [audit-logs] Exported audit logs from UI now would have "BEFORE" and "AFTER" fields
44
- [core] Ability to mark reports as 'dirty' to make sure they are regenerated in full
@@ -46,6 +46,18 @@ Dependencies:
4646
- Bump sass from 1.81.0 to 1.83.3
4747
- Bump tslib from 2.7.0 to 2.8.1
4848

49+
## Version 24.10.8
50+
Fixes:
51+
- [app-management] Unescaping HTML for created/updated application names
52+
- [countly-edge] Add import from Countly Edge Server
53+
- [languages] Calculated total users percentage correctly
54+
55+
Features:
56+
- [hooks] Support sending header information for HTTP actions
57+
58+
Enterprise fixes:
59+
- [ab-testing] Mismatching user counts between ab-testing and user profiles
60+
4961
## Version 24.10.7
5062
Fixes:
5163
- [data-manager] Modifying existing values when segment values want to be updated in the Data Manager

frontend/express/public/core/app-management/javascripts/countly.views.js

+2
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,7 @@
387387
countlyGlobal.admin_apps[data._id] = data;
388388
Backbone.history.appIds.push(data._id + "");
389389
countlyGlobal.apps[data._id].image = "appimages/" + data._id + ".png?" + Date.now().toString();
390+
self.apps[data._id].name = countlyCommon.unescapeHtml(data.name);
390391
self.appList.push({
391392
value: data._id + "",
392393
label: data.name
@@ -450,6 +451,7 @@
450451
countlyGlobal.admin_apps[self.selectedApp][modAttr] = data[modAttr];
451452
}
452453
countlyGlobal.apps[self.selectedApp].label = data.name;
454+
self.apps[self.selectedApp].name = countlyCommon.unescapeHtml(data.name);
453455
for (var i = 0; i < self.appList.length; i++) {
454456
if (self.appList[i].value === self.selectedApp) {
455457
self.appList[i].label = data.name;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<title>Countly Edge - Import Data</title>
5+
<link rel="icon" type="image/x-icon" href="../images/favicon.png">
6+
<style>
7+
:root {
8+
--primary-color: #0166D6;
9+
--background-color: #F7F7F7;
10+
--border-color: #ECECEC;
11+
--text-color: #333333;
12+
--success-color: #2FA732;
13+
--error-color: #D63E40;
14+
}
15+
16+
body {
17+
font-family: -apple-system, Inter, system-ui, sans-serif;
18+
background-color: var(--background-color);
19+
color: var(--text-color);
20+
margin: 0;
21+
padding: 2rem;
22+
line-height: 1.5;
23+
}
24+
25+
.container {
26+
max-width: 1000px;
27+
margin: 0 auto;
28+
background: white;
29+
padding: 2rem;
30+
border-radius: 12px;
31+
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
32+
}
33+
34+
h1 {
35+
color: var(--text-color);
36+
font-size: 24px;
37+
font-weight: 600;
38+
margin: 0 0 2rem;
39+
padding-bottom: 1rem;
40+
border-bottom: 1px solid var(--border-color);
41+
}
42+
43+
.form-group {
44+
margin-bottom: 1.5rem;
45+
}
46+
47+
label {
48+
display: block;
49+
margin-bottom: 0.5rem;
50+
font-weight: 500;
51+
color: var(--text-color);
52+
}
53+
54+
input {
55+
width: 100%;
56+
padding: 0.75rem;
57+
border: 1px solid var(--border-color);
58+
border-radius: 6px;
59+
font-size: 14px;
60+
transition: border-color 0.2s;
61+
box-sizing: border-box;
62+
}
63+
64+
input:focus {
65+
outline: none;
66+
border-color: var(--primary-color);
67+
}
68+
69+
input[type="file"] {
70+
padding: 0.5rem;
71+
background: var(--background-color);
72+
}
73+
74+
button {
75+
background: var(--primary-color);
76+
color: white;
77+
border: none;
78+
padding: 0.75rem 1.5rem;
79+
border-radius: 6px;
80+
font-weight: 500;
81+
cursor: pointer;
82+
transition: opacity 0.2s;
83+
font-size: 14px;
84+
}
85+
86+
button:hover {
87+
opacity: 0.9;
88+
}
89+
90+
button:disabled {
91+
background: var(--border-color);
92+
cursor: not-allowed;
93+
}
94+
95+
.progress-container {
96+
margin-top: 2rem;
97+
display: none;
98+
}
99+
100+
.progress {
101+
background: var(--background-color);
102+
border-radius: 6px;
103+
overflow: hidden;
104+
height: 8px;
105+
}
106+
107+
.progress-bar {
108+
height: 100%;
109+
background: var(--primary-color);
110+
width: 0;
111+
transition: width 0.3s ease;
112+
}
113+
114+
#status {
115+
margin-top: 1rem;
116+
padding: 1rem;
117+
border-radius: 6px;
118+
font-size: 14px;
119+
}
120+
121+
.success {
122+
background: rgba(47, 167, 50, 0.1);
123+
color: var(--success-color);
124+
border: 1px solid rgba(47, 167, 50, 0.2);
125+
}
126+
127+
.error {
128+
background: rgba(214, 62, 64, 0.1);
129+
color: var(--error-color);
130+
border: 1px solid rgba(214, 62, 64, 0.2);
131+
}
132+
133+
.header {
134+
display: flex;
135+
align-items: center;
136+
margin-bottom: 2rem;
137+
}
138+
139+
.header img {
140+
height: 32px;
141+
margin-right: 1rem;
142+
}
143+
</style>
144+
</head>
145+
<body>
146+
<div class="container">
147+
<div class="header">
148+
<img src="../images/pre-login/countly-logo-dark.svg" alt="Countly">
149+
</div>
150+
<div class="header">
151+
<h1>Import Data</h1>
152+
</div>
153+
<form id="importForm">
154+
<div class="form-group">
155+
<label for="file">Export File (JSON or CSV)</label>
156+
<input type="file" id="file" accept=".json,.csv" required>
157+
</div>
158+
<div class="form-group">
159+
<label for="server">Server URL</label>
160+
<input type="url" id="server" required>
161+
</div>
162+
<div class="form-group">
163+
<label for="batchSize">Batch Size</label>
164+
<input type="number" id="batchSize" value="10" min="1" max="100" required>
165+
</div>
166+
<div class="form-group">
167+
<label for="delay">Delay between batches (ms)</label>
168+
<input type="number" id="delay" value="1000" min="0" required>
169+
</div>
170+
<div class="form-group">
171+
<button type="submit">Start Import</button>
172+
</div>
173+
</form>
174+
<div class="progress-container">
175+
<div class="progress">
176+
<div class="progress-bar"></div>
177+
</div>
178+
<div id="status"></div>
179+
</div>
180+
</div>
181+
182+
<script>
183+
const form = document.getElementById('importForm');
184+
const serverInput = document.getElementById('server');
185+
const progressBar = document.querySelector('.progress-bar');
186+
const progressContainer = document.querySelector('.progress-container');
187+
const status = document.getElementById('status');
188+
189+
// Set default server URL from current location or fallback
190+
function setDefaultServer() {
191+
const defaultServer = 'http://localhost:3000';
192+
try {
193+
const currentUrl = new URL(window.location.href);
194+
serverInput.value = `${currentUrl.protocol}//${currentUrl.host}`;
195+
} catch (error) {
196+
serverInput.value = defaultServer;
197+
}
198+
}
199+
200+
// Call on page load
201+
setDefaultServer();
202+
203+
async function processFile(file) {
204+
const text = await file.text();
205+
if (file.name.endsWith('.csv')) {
206+
return text
207+
.split('\n')
208+
.slice(1)
209+
.filter(line => line.trim())
210+
.map(line => {
211+
const [, , data] = line.split(',');
212+
return JSON.parse(data);
213+
});
214+
} else {
215+
const json = JSON.parse(text);
216+
return json.records.map(r => r.data);
217+
}
218+
}
219+
220+
async function sleep(ms) {
221+
return new Promise(resolve => setTimeout(resolve, ms));
222+
}
223+
224+
form.addEventListener('submit', async (e) => {
225+
e.preventDefault();
226+
const file = document.getElementById('file').files[0];
227+
if (!file) return;
228+
229+
const server = document.getElementById('server').value;
230+
const batchSize = parseInt(document.getElementById('batchSize').value);
231+
const delay = parseInt(document.getElementById('delay').value);
232+
233+
try {
234+
form.querySelector('button').disabled = true;
235+
progressContainer.style.display = 'block';
236+
status.className = '';
237+
status.textContent = 'Processing file...';
238+
239+
const records = await processFile(file);
240+
let processed = 0;
241+
242+
for (let i = 0; i < records.length; i += batchSize) {
243+
const batch = records.slice(i, i + batchSize);
244+
try {
245+
const response = await fetch(`${server}/i/bulk`, {
246+
method: 'POST',
247+
headers: {
248+
'Content-Type': 'application/json'
249+
},
250+
body: JSON.stringify({ requests: batch })
251+
});
252+
253+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
254+
255+
processed += batch.length;
256+
const percent = (processed / records.length * 100).toFixed(1);
257+
progressBar.style.width = percent + '%';
258+
status.textContent = `Processed ${processed}/${records.length} records (${percent}%)`;
259+
260+
if (i + batchSize < records.length) {
261+
await sleep(delay);
262+
}
263+
} catch (error) {
264+
throw new Error(`Error sending batch at index ${i}: ${error.message}`);
265+
}
266+
}
267+
268+
status.className = 'success';
269+
status.textContent = `Import completed! Processed ${processed} records.`;
270+
} catch (error) {
271+
status.className = 'error';
272+
status.textContent = `Import failed: ${error.message}`;
273+
} finally {
274+
form.querySelector('button').disabled = false;
275+
}
276+
});
277+
</script>
278+
</body>
279+
</html>

plugins/hooks/api/api.js

+1
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,7 @@ const CheckEffectProperties = function(effect) {
233233
if (effect) {
234234
if (effect.type === "HTTPEffect") {
235235
rules.url = { 'required': true, 'type': 'URL', 'regex': '^(?!.*(?:localhost|127\\.0\\.0\\.1|\\[::1\\])).*(?:https?|ftp):\\/\\/(?:[a-zA-Z0-9-]+(?:\\.[a-zA-Z0-9-]+)+|\\[(?:[a-fA-F0-9]{1,4}:){7}[a-fA-F0-9]{1,4}\\])(?::\\d{1,5})?(?:\\/\\S*)?$' };
236+
rules.headers = { 'required': false, 'type': 'Object' };
236237
}
237238
}
238239
return rules;

plugins/hooks/api/parts/effects/http.js

+8-3
Original file line numberDiff line numberDiff line change
@@ -41,19 +41,23 @@ class HTTPEffect {
4141
async run(options) {
4242
const logs = [];
4343
const {effect, params, rule, effectStep, _originalInput} = options;
44-
const {method, url, requestData} = effect.configuration;
44+
const {method, url, requestData, headers} = effect.configuration;
4545
try {
4646
const parsedURL = utils.parseStringTemplate(url, params);
4747
const parsedRequestData = utils.parseStringTemplate(requestData, params, method);
4848
log.d("[hook http effect ]", parsedURL, parsedRequestData, method);
4949

5050
// todo: assemble params for request;
5151
// const params = {}
52-
52+
const requestHeaders = headers || {};
5353
const methodOption = method && method.toLowerCase() || "get";
5454
switch (methodOption) {
5555
case 'get':
56-
await request.get({uri: parsedURL + "?" + parsedRequestData, timeout: this._timeout}, function(e, r, body) {
56+
await request.get({
57+
uri: parsedURL + "?" + parsedRequestData,
58+
timeout: this._timeout,
59+
headers: requestHeaders
60+
}, function(e, r, body) {
5761
log.d("[http get effect]", e, body);
5862
if (e) {
5963
logs.push(`Error: ${e.message}`);
@@ -91,6 +95,7 @@ class HTTPEffect {
9195
uri: parsedURL,
9296
json: parsedJSON,
9397
timeout: this._timeout,
98+
headers: requestHeaders
9499
},
95100
function(e, r, body) {
96101
log.d("[httpeffects]", e, body, rule);

0 commit comments

Comments
 (0)