Skip to content

Commit b68e2a4

Browse files
committed
Merge branch 'master' into ar2rsawseen/master2
# Conflicts: # package-lock.json
2 parents a39fce4 + 226c171 commit b68e2a4

File tree

8 files changed

+177
-74
lines changed

8 files changed

+177
-74
lines changed

.github/workflows/main.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ jobs:
243243
node bin/scripts/test.connection.js
244244
npx grunt mochaTest
245245
ui-test-dashboard:
246-
runs-on: ubuntu-latest
246+
runs-on: ubuntu-22.04-4core
247247

248248
services:
249249
mongodb:
@@ -326,7 +326,7 @@ jobs:
326326
curl -o /tmp/uploader.log -u "${{ secrets.BOX_UPLOAD_AUTH }}" ${{ secrets.BOX_UPLOAD_PATH }} -T "$ARTIFACT_ARCHIVE_NAME"
327327
328328
ui-test-onboarding:
329-
runs-on: ubuntu-latest
329+
runs-on: ubuntu-22.04-4core
330330

331331
services:
332332
mongodb:

CHANGELOG.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,25 @@
1-
## Version 25.03.x
1+
## Version 25.03.6
22
Enterprise Features:
3+
- [journey_engine] Publish toggle is converted into button and rearranged
34
- [journey_engine] Record Event block added
45
- [llm interaction] Add LLM interaction event
56
- [oidc] PKCE Flow support added
67

78
Fixes:
9+
- [core] Fix auto refresh frontend component
810
- [core] Unifying alphabetical order for dropdowns with dashboard apps
11+
- [formulas] Fix loading state when selecting event count
912
- [hooks] Added null check for incoming data
10-
- [core] Fix auto refresh frontend component
11-
- [times-of-day] Fix chart component
1213
- [push] Fix external drawer initialization
14+
- [times-of-day] Fix chart component
1315

1416
Enterprise Fixes:
1517
- [content] Asset URL was wrongly constructed when user switches between apps
1618
- [ab-testing] Updates
1719
- Do not wait for result calculation when requesting experiments
1820
- Do not calculate result for completed experiments
1921
- [drill] [license] Shorten warning period from 14 days to 3 days
22+
- [drill] Fix query for users in drill that leads to severe server slowdown
2023
- [license] Fix chart legend
2124

2225
Dependencies:

bin/scripts/fix-data/user-merge.js

Lines changed: 98 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,103 +1,146 @@
11
/**
2-
* Description: This script is used to merge users based on username.
2+
* Description: This script merges users if they match on any fields configured in the script.
3+
* configure - processAllFields function to add or remove fields for merging.
34
* Server: countly
45
* Path: $(countly dir)/bin/scripts/fix-data
5-
* Command: node user-merge.js
6+
* Command: node user-merge.js --no-dry-run [--record-overload-sleep 1000]
7+
* --record-overload-sleep: Cooldown period when record count exceeds RECORD_COUNT_LIMIT
68
*/
79
var pluginManager = require("../../../plugins/pluginManager.js");
810
var appUsers = require("../../../api/parts/mgmt/app_users.js");
911
var common = require("../../../api/utils/common.js");
1012

11-
console.log("Merging app users");
12-
1313
var APP_ID = "";
1414
var COLLECTION_NAME = "app_users" + APP_ID;
1515

16+
if (!APP_ID) {
17+
console.error("Please set APP_ID variable to the ID of the app you want to merge users for.");
18+
process.exit(1);
19+
}
20+
1621
var RETRY_LIMIT = 3;
1722
var UPDATE_COUNTER = 0;
18-
1923
//Number of requests to be made before checking record count in app_user_merges
2024
var UPDATE_LIMIT = 100;
2125
//Number of records in app_user_merges after which script will sleep
2226
var RECORD_COUNT_LIMIT = 10;
2327
//Cooldown period if record count exceeds limit
2428
var RECORD_OVERLOAD_SLEEP = 2000;
29+
for (let i = 2; i < process.argv.length; i++) {
30+
if (process.argv[i] === '--record-overload-sleep' && process.argv[i + 1]) {
31+
RECORD_OVERLOAD_SLEEP = parseInt(process.argv[i + 1]);
32+
break;
33+
}
34+
}
2535
//Cooldown period between requests
2636
var COOLDOWN_PERIOD = 1000;
2737

38+
// Check for dry run flag
39+
let DRY_RUN = true;
40+
if (process.argv.includes('--no-dry-run')) {
41+
DRY_RUN = false;
42+
}
43+
console.log(DRY_RUN ? "Running in DRY RUN mode - no actual merges will be performed" : "Running in LIVE mode - merges will be performed");
44+
45+
console.log("Merging app users");
46+
2847
const sleep = m => new Promise((r) => {
29-
//console.log("Cooling period for " + m + " seconds!");
3048
setTimeout(r, m);
3149
});
3250

3351
pluginManager.dbConnection("countly").then(async(countlyDb) => {
3452
try {
35-
3653
common.db = countlyDb;
37-
38-
await cursor();
39-
40-
console.log("Total updates on the server - ", UPDATE_COUNTER);
41-
console.log("Script ran successfully!");
54+
await processAllFields();
55+
console.log("Total potential merges found - ", UPDATE_COUNTER);
56+
if (DRY_RUN) {
57+
console.log("Dry run completed - no actual merges were performed");
58+
}
59+
else {
60+
console.log("All merges completed successfully!");
61+
}
4262
common.db.close();
43-
process.exit(1);
63+
process.exit(0);
4464
}
4565
catch (e) {
4666
console.log("Error while running script ", e);
4767
common.db.close();
4868
process.exit(1);
4969
}
5070

51-
async function cursor() {
71+
async function processAllFields() {
72+
//await processDuplicates('email'); we can also run multiple merges one after the other based on different fields
73+
await processDuplicates('name');
74+
}
75+
76+
async function processDuplicates(field) {
77+
console.log(`\nProcessing duplicates by ${field}`);
5278

5379
const duplicates = await common.db.collection(COLLECTION_NAME).aggregate([
80+
{
81+
$match: {
82+
[field]: { $nin: [null, ""], $exists: true } // Only match non-null, non-empty values
83+
}
84+
},
5485
{
5586
$group: {
56-
_id: "$username",
87+
_id: `$${field}`,
5788
count: { $sum: 1 }
5889
}
5990
},
6091
{
6192
$match: {
62-
count: { $gt: 1 },
63-
_id: { $ne: null }
93+
count: { $gt: 1 }
6494
}
6595
}
6696
]).toArray();
6797

68-
console.log("Found", duplicates.length, "duplicate username groups.");
98+
console.log(`Found ${duplicates.length} duplicate groups for ${field}`);
6999

70-
for (var i = 0; i < duplicates.length; i++) {
100+
for (const duplicate of duplicates) {
101+
const query = { [field]: duplicate._id };
71102

72-
var mainUser = null;
73-
var mergedUsersUIDs = [];
103+
const cursor = common.db.collection(COLLECTION_NAME)
104+
.find(query)
105+
.sort({ lac: -1 });
74106

75-
var query = {
76-
username: duplicates[i]._id
77-
};
107+
let mainUser = null;
108+
let mergedUIDs = 0;
78109

79-
var projections = {};
110+
console.log(`\n${DRY_RUN ? '[DRY RUN] Would merge' : 'Merging'} users matching ${field}: "${duplicate._id}"`);
80111

81-
var sort = { ls: -1 };
82-
83-
var cursor = common.db.collection(COLLECTION_NAME).find(query).project(projections).sort(sort);
84-
85-
while (await cursor.hasNext()) {
86-
var doc = await cursor.next();
112+
for await (const user of cursor) {
113+
if (!mainUser) {
114+
mainUser = user;
115+
console.log('Main user would be:', {
116+
uid: mainUser.uid,
117+
email: mainUser.email || "null",
118+
phone: mainUser.phone || "null",
119+
name: mainUser.name || "null",
120+
last_action: formatLac(mainUser.lac)
121+
});
122+
continue;
123+
}
87124

88-
if (doc.uid && doc.uid !== "") {
89-
if (!mainUser) {
90-
mainUser = doc;
91-
}
92-
else {
93-
await mergeUsers(mainUser, doc);
94-
mergedUsersUIDs.push(doc.uid);
125+
if (user.uid && user.uid !== "") {
126+
console.log('Would merge user:', {
127+
uid: user.uid,
128+
email: user.email || "null",
129+
phone: user.phone || "null",
130+
name: user.name || "null",
131+
last_action: formatLac(user.lac)
132+
});
133+
134+
if (!DRY_RUN) {
135+
await mergeUsers(mainUser, user);
95136
}
137+
mergedUIDs++;
138+
UPDATE_COUNTER++;
96139
}
97140
}
98141

99-
if (mergedUsersUIDs.length > 0) {
100-
console.log("Total", mergedUsersUIDs.length, "users merged into user", mainUser.uid, ": (", mergedUsersUIDs.join(", "), ")");
142+
if (mergedUIDs > 0) {
143+
console.log(`${DRY_RUN ? '[DRY RUN] Would merge' : 'Merged'} ${mergedUIDs} users into ${mainUser.uid}`);
101144
}
102145
}
103146
}
@@ -118,7 +161,6 @@ pluginManager.dbConnection("countly").then(async(countlyDb) => {
118161
else {
119162
success = true;
120163
}
121-
122164
resolve();
123165
});
124166
});
@@ -129,7 +171,6 @@ pluginManager.dbConnection("countly").then(async(countlyDb) => {
129171
if (retryCounter > 1) {
130172
console.log("User ", user.uid, " merged successfully after ", retryCounter, " retries.");
131173
}
132-
UPDATE_COUNTER += 1;
133174
if (UPDATE_COUNTER % UPDATE_LIMIT === 0) {
134175
await checkRecordCount();
135176
}
@@ -140,13 +181,27 @@ pluginManager.dbConnection("countly").then(async(countlyDb) => {
140181
}
141182

142183
async function checkRecordCount() {
184+
if (DRY_RUN) {
185+
return;
186+
}
187+
143188
var recordCount = await common.db.collection("app_user_merges").countDocuments();
144189
console.log("Record count in app_user_merges: ", recordCount);
145190

146191
while (recordCount > RECORD_COUNT_LIMIT) {
147-
console.log("Record count exceeds limit. Sleeping for " + RECORD_OVERLOAD_SLEEP / 1000 + "seconds.");
192+
console.log("Record count exceeds limit. Sleeping for " + RECORD_OVERLOAD_SLEEP / 1000 + " seconds.");
148193
await sleep(RECORD_OVERLOAD_SLEEP);
149194
recordCount = await common.db.collection("app_user_merges").countDocuments();
150195
}
151196
}
197+
198+
function formatLac(timestamp) {
199+
if (!timestamp) {
200+
return null;
201+
}
202+
if (Math.round(timestamp).toString().length === 10) {
203+
timestamp *= 1000;
204+
}
205+
return new Date(timestamp);
206+
}
152207
});

frontend/express/public/javascripts/countly/countly.event.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
_period = null;
1818
var _activeLoadedEvent = "";
1919
var _activeLoadedSegmentation = "";
20+
var _refreshEventsPromise = null;
2021

2122
countlyEvent.hasLoadedData = function() {
2223
if (_activeLoadedEvent && _activeLoadedEvent === _activeEvent && _activeLoadedSegmentation === _activeSegmentation) {
@@ -497,7 +498,11 @@
497498

498499
countlyEvent.refreshEvents = function() {
499500
if (!countlyCommon.DEBUG) {
500-
return $.ajax({
501+
if (_refreshEventsPromise) {
502+
return _refreshEventsPromise;
503+
}
504+
505+
_refreshEventsPromise = $.ajax({
501506
type: "GET",
502507
url: countlyCommon.API_PARTS.data.r,
503508
data: {
@@ -510,8 +515,13 @@
510515
if (!_activeEvent && countlyEvent.getEvents()[0]) {
511516
_activeEvent = countlyEvent.getEvents()[0].key;
512517
}
518+
},
519+
complete: function() {
520+
_refreshEventsPromise = null;
513521
}
514522
});
523+
524+
return _refreshEventsPromise;
515525
}
516526
else {
517527
_activeEvents = {};

frontend/express/public/javascripts/countly/vue/components/content.js

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@
6666
type: String
6767
},
6868

69+
saveButtonTooltip: {
70+
default: null,
71+
type: String
72+
},
73+
6974
status: {
7075
default: () => ({
7176
label: 'Status',
@@ -119,11 +124,13 @@
119124
'tab-change'
120125
],
121126

122-
data: () => ({
123-
currentTab: null,
124-
125-
isReadonlyInput: true
126-
}),
127+
data() {
128+
return {
129+
currentTab: null,
130+
isReadonlyInput: true,
131+
showActionsPopup: false
132+
};
133+
},
127134

128135
computed: {
129136
activeTab: {
@@ -136,6 +143,15 @@
136143
}
137144
},
138145

146+
localValue: {
147+
get() {
148+
return countlyCommon.unescapeHtml(this.value);
149+
},
150+
set(value) {
151+
this.$emit('input', value);
152+
}
153+
},
154+
139155
closeButtonIcon() {
140156
return this.closeButton ? 'cly-io-x' : 'cly-io-arrow-sm-left';
141157
},
@@ -152,15 +168,6 @@
152168
return !!this.options.length;
153169
},
154170

155-
localValue: {
156-
get() {
157-
return countlyCommon.unescapeHtml(this.value);
158-
},
159-
set(value) {
160-
this.$emit('input', value);
161-
}
162-
},
163-
164171
toggleLocalValue: {
165172
get() {
166173
return this.toggleValue;
@@ -196,6 +203,10 @@
196203
this.$emit('save');
197204
},
198205

206+
onPublishButtonClick() {
207+
this.toggleLocalValue = !this.toggleLocalValue;
208+
},
209+
199210
toggleInputReadonlyState() {
200211
this.isReadonlyInput = !this.isReadonlyInput;
201212
}

0 commit comments

Comments
 (0)