data-primals-engine 1.4.2 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +878 -856
- package/client/package-lock.json +82 -0
- package/client/package.json +2 -0
- package/client/src/App.jsx +1 -1
- package/client/src/App.scss +25 -7
- package/client/src/AssistantChat.scss +3 -2
- package/client/src/ConditionBuilder.jsx +1 -1
- package/client/src/ConditionBuilder2.jsx +2 -1
- package/client/src/DashboardView.jsx +569 -569
- package/client/src/DataEditor.jsx +376 -368
- package/client/src/DataLayout.jsx +4 -10
- package/client/src/DataTable.jsx +858 -817
- package/client/src/Field.jsx +1825 -1784
- package/client/src/FlexDataRenderer.jsx +2 -0
- package/client/src/FlexTreeUtils.js +1 -1
- package/client/src/GeolocationField.jsx +94 -0
- package/client/src/KPIDialog.jsx +11 -1
- package/client/src/ModelCreator.jsx +1 -2
- package/client/src/ModelCreatorField.jsx +24 -27
- package/client/src/ModelList.jsx +1 -1
- package/client/src/RelationField.jsx +2 -2
- package/client/src/constants.js +3 -3
- package/client/src/filter.js +0 -155
- package/client/src/hooks/useTutorials.jsx +62 -65
- package/client/src/translations.js +14 -2
- package/package.json +2 -1
- package/perf/README.md +147 -0
- package/perf/artillery-hooks.js +37 -0
- package/perf/perf-shot-hardwork.yml +84 -0
- package/perf/perf-shot-search.yml +45 -0
- package/perf/setup.yml +26 -0
- package/server.js +1 -1
- package/src/constants.js +264 -31
- package/src/core.js +15 -1
- package/src/data.js +1 -1
- package/src/defaultModels.js +1544 -1540
- package/src/email.js +5 -2
- package/src/engine.js +10 -3
- package/src/filter.js +274 -260
- package/src/i18n.js +187 -177
- package/src/modules/assistant/assistant.js +3 -1
- package/src/modules/bucket.js +12 -15
- package/src/modules/data/data.backup.js +11 -8
- package/src/modules/data/data.core.js +2 -1
- package/src/modules/data/data.js +6 -3
- package/src/modules/data/data.operations.js +610 -168
- package/src/modules/data/data.routes.js +1821 -1785
- package/src/modules/data/data.scheduling.js +2 -1
- package/src/modules/data/data.validation.js +7 -1
- package/src/modules/file.js +4 -2
- package/src/modules/user.js +4 -1
- package/src/modules/workflow.js +9 -10
- package/src/openai.jobs.js +2 -0
- package/src/packs.js +22 -5
- package/src/providers.js +22 -7
- package/swagger-en.yml +133 -0
- package/test/data.integration.test.js +1060 -981
- package/test/import_export.integration.test.js +1 -1
- package/test/model.integration.test.js +377 -221
|
@@ -4,7 +4,7 @@ import { useUI } from '../contexts/UIContext.jsx';
|
|
|
4
4
|
import { useNotificationContext } from '../NotificationProvider.jsx';
|
|
5
5
|
import { tutorialsConfig } from '../tutorials.js';
|
|
6
6
|
import { useTranslation } from 'react-i18next';
|
|
7
|
-
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
|
7
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
8
8
|
import useLocalStorage from "./useLocalStorage.js";
|
|
9
9
|
import {Event} from "../../../src/events.js";
|
|
10
10
|
|
|
@@ -20,9 +20,9 @@ export const useTutorials = () => {
|
|
|
20
20
|
const { allTourSteps, setCurrentTourSteps, setIsTourOpen } = useUI();
|
|
21
21
|
const { addNotification } = useNotificationContext();
|
|
22
22
|
|
|
23
|
-
//
|
|
24
|
-
|
|
25
|
-
const
|
|
23
|
+
// Ref pour "verrouiller" la vérification d'une étape spécifique et éviter les notifications en double
|
|
24
|
+
// à cause des re-renders rapides (race condition).
|
|
25
|
+
const completingStepRef = useRef(null);
|
|
26
26
|
|
|
27
27
|
const { data: tutorials, isLoading: isLoadingTutorials } = useQuery('tutorials', async () => {
|
|
28
28
|
return tutorialsConfig.map(t => ({ ...t, _id: t.id }));
|
|
@@ -119,102 +119,99 @@ export const useTutorials = () => {
|
|
|
119
119
|
updateActiveTutorial(newActiveState);
|
|
120
120
|
launchTour(firstStage.tourName);
|
|
121
121
|
};
|
|
122
|
-
|
|
122
|
+
|
|
123
123
|
const triggerTutorialCheck = useCallback(async () => {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
124
|
+
// S'il n'y a pas de tutoriel actif, on ne fait rien.
|
|
125
|
+
if (!me?.activeTutorial || !tutorials) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const { id: currentTutorialId, stage: currentStageNumber } = me.activeTutorial;
|
|
130
|
+
const stepIdentifier = `${currentTutorialId}-${currentStageNumber}`;
|
|
131
|
+
|
|
132
|
+
// --- VERROUILLAGE (LOCK) ---
|
|
133
|
+
// Si on est déjà en train de traiter cette étape exacte, on ignore les appels suivants
|
|
134
|
+
// pour éviter les notifications multiples.
|
|
135
|
+
if (completingStepRef.current === stepIdentifier) {
|
|
136
|
+
console.log(`[Tutoriels] Vérification pour l'étape ${stepIdentifier} déjà en cours. On ignore.`);
|
|
127
137
|
return;
|
|
128
138
|
}
|
|
129
|
-
if (!me?.activeTutorial || !tutorials) return;
|
|
130
139
|
|
|
131
|
-
|
|
132
|
-
|
|
140
|
+
const tutorial = tutorials.find(t => t._id === currentTutorialId);
|
|
141
|
+
const stageConfig = tutorial?.stages.find(s => s.stage === currentStageNumber);
|
|
142
|
+
|
|
143
|
+
if (!stageConfig) return; // Pas d'étape active à vérifier
|
|
133
144
|
|
|
134
145
|
try {
|
|
135
|
-
|
|
146
|
+
// On pose le verrou pour cette étape
|
|
147
|
+
completingStepRef.current = stepIdentifier;
|
|
148
|
+
console.log(`[Tutoriels] Verrouillage et vérification de l'étape ${stepIdentifier}`);
|
|
136
149
|
|
|
137
|
-
|
|
138
|
-
const tutorial = tutorials.find(t => t._id === currentActiveState.id);
|
|
139
|
-
if (!tutorial) break;
|
|
150
|
+
const { isCompleted } = await checkCompletionCondition(stageConfig.completionCondition);
|
|
140
151
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
console.log('[Tutoriels] Fin du tutoriel, attribution des récompenses.');
|
|
144
|
-
claimRewards(tutorial._id);
|
|
145
|
-
updateActiveTutorial(null);
|
|
146
|
-
setIsTourOpen(false);
|
|
147
|
-
break;
|
|
148
|
-
}
|
|
152
|
+
if (isCompleted) {
|
|
153
|
+
console.log(`[Tutoriels] Étape ${currentStageNumber} complétée.`);
|
|
149
154
|
|
|
150
|
-
const
|
|
155
|
+
const titleKey = `tutorial.${tutorial.id}.stage.${currentStageNumber}.name`;
|
|
156
|
+
const descriptionKey = `tutorial.${tutorial.id}.stage.${currentStageNumber}.description`;
|
|
151
157
|
|
|
152
|
-
|
|
153
|
-
|
|
158
|
+
addNotification({
|
|
159
|
+
title: t(titleKey, stageConfig.name),
|
|
160
|
+
message: t(descriptionKey, stageConfig.description),
|
|
161
|
+
status: 'success',
|
|
162
|
+
id: `tuto-step-${stepIdentifier}` // ID unique pour la notification
|
|
163
|
+
});
|
|
154
164
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
165
|
+
// On passe à l'étape suivante (ou on termine le tutoriel)
|
|
166
|
+
const nextStageNumber = currentStageNumber + 1;
|
|
167
|
+
const nextStage = tutorial.stages.find(s => s.stage === nextStageNumber);
|
|
158
168
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
status: 'success'
|
|
163
|
-
});
|
|
164
|
-
currentActiveState = { id: currentActiveState.id, stage: currentActiveState.stage + 1 };
|
|
169
|
+
if (nextStage) {
|
|
170
|
+
updateActiveTutorial({ id: currentTutorialId, stage: nextStageNumber });
|
|
171
|
+
launchTour(nextStage.tourName);
|
|
165
172
|
} else {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
launchTour(stageConfig.tourName);
|
|
170
|
-
}
|
|
171
|
-
break;
|
|
173
|
+
claimRewards(tutorial._id);
|
|
174
|
+
updateActiveTutorial(null);
|
|
175
|
+
setIsTourOpen(false);
|
|
172
176
|
}
|
|
177
|
+
} else {
|
|
178
|
+
console.log(`[Tutoriels] Étape ${currentStageNumber} non complétée.`);
|
|
173
179
|
}
|
|
174
180
|
} catch (error) {
|
|
175
|
-
console.error("[Tutoriels] Erreur lors de la vérification
|
|
181
|
+
console.error("[Tutoriels] Erreur lors de la vérification de l'étape :", error);
|
|
176
182
|
} finally {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
// Si une vérification a été mise en attente, on la lance maintenant.
|
|
181
|
-
if (isCheckQueuedRef.current) {
|
|
182
|
-
console.log('[Tutoriels] Lancement de la vérification mise en file d\'attente.');
|
|
183
|
-
isCheckQueuedRef.current = false;
|
|
184
|
-
triggerTutorialCheck();
|
|
185
|
-
}
|
|
183
|
+
// On libère le verrou
|
|
184
|
+
completingStepRef.current = null;
|
|
185
|
+
console.log(`[Tutoriels] Libération du verrou pour l'étape ${stepIdentifier}`);
|
|
186
186
|
}
|
|
187
|
-
}, [
|
|
188
|
-
tutorials, checkCompletionCondition, claimRewards, updateActiveTutorial,
|
|
189
|
-
addNotification, launchTour, t, setIsTourOpen, me
|
|
190
|
-
]);
|
|
187
|
+
}, [me, tutorials, checkCompletionCondition, updateActiveTutorial, claimRewards, addNotification, launchTour, t, setIsTourOpen]);
|
|
191
188
|
|
|
192
189
|
useEffect(() => {
|
|
193
190
|
setMe(prevMe => ({ ...prevMe, activeTutorial: activeTuto }));
|
|
194
191
|
}, [activeTuto]);
|
|
195
192
|
|
|
193
|
+
const handleDataChange = useCallback(async (payload) => {
|
|
194
|
+
if (me?.activeTutorial) {
|
|
195
|
+
console.log(`[Tutoriels] Événement de données reçu, déclenchement de la vérification.`, payload);
|
|
196
|
+
triggerTutorialCheck();
|
|
197
|
+
}
|
|
198
|
+
}, [me?.activeTutorial, triggerTutorialCheck]);
|
|
199
|
+
|
|
196
200
|
// Écoute les événements de modification de données pour vérifier la progression en arrière-plan.
|
|
197
201
|
useEffect(() => {
|
|
198
|
-
const handleDataChange = async (payload) => {
|
|
199
|
-
console.log(`[Tutoriels] Événement de données reçu.`, payload);
|
|
200
|
-
if (me?.activeTutorial) {
|
|
201
|
-
triggerTutorialCheck();
|
|
202
|
-
}
|
|
203
|
-
};
|
|
204
|
-
|
|
205
|
-
console.log(me.activeTutorial + new Date().getMilliseconds());
|
|
206
202
|
const eventTypes = ['API_ADD_DATA', 'API_EDIT_DATA', 'API_DELETE_DATA'];
|
|
207
203
|
eventTypes.forEach(type => Event.Listen(type, handleDataChange, "custom", "data"));
|
|
208
204
|
|
|
209
205
|
return () => {
|
|
210
206
|
eventTypes.forEach(type => Event.RemoveCallback(type, handleDataChange, "custom", "data"));
|
|
211
207
|
};
|
|
212
|
-
}, [
|
|
208
|
+
}, [handleDataChange]);
|
|
213
209
|
|
|
214
210
|
// Vérifie la progression lorsqu'un tutoriel devient actif ou que son étape change.
|
|
215
211
|
useEffect(() => {
|
|
216
212
|
triggerTutorialCheck();
|
|
217
|
-
},[]);
|
|
213
|
+
},[me?.activeTutorial]); // Se déclenche quand le tutoriel/étape change
|
|
214
|
+
|
|
218
215
|
return {
|
|
219
216
|
tutorials,
|
|
220
217
|
isLoadingTutorials,
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
export const websiteTranslations = {
|
|
3
3
|
fr: {
|
|
4
4
|
translation: {
|
|
5
|
+
"dashboards.addKpi": "Ajouter un indicateur KPI",
|
|
6
|
+
|
|
7
|
+
"field.geolocation": "Géolocalisation",
|
|
5
8
|
"time.minute": "Minute",
|
|
6
9
|
"time.hour": "Heure",
|
|
7
10
|
"time.day": "Jour",
|
|
@@ -55,8 +58,6 @@ export const websiteTranslations = {
|
|
|
55
58
|
"history.revertSuccess": "Document restauré avec succès. L'historique va maintenant être mis à jour.",
|
|
56
59
|
"history.revertError": "Erreur lors de la restauration : {{error}}",
|
|
57
60
|
|
|
58
|
-
"field_endpoint_isPublic": "Accès public",
|
|
59
|
-
"field_endpoint_isPublic_hint": "Si coché, ce point d'accès sera accessible sans authentification. Le script s'exécutera avec les droits du propriétaire du point d'accès.",
|
|
60
61
|
|
|
61
62
|
"packs.manualInstall": "Importer",
|
|
62
63
|
"packs.manualInstall.title": "Installation manuelle de pack",
|
|
@@ -606,6 +607,7 @@ export const websiteTranslations = {
|
|
|
606
607
|
},
|
|
607
608
|
en: {
|
|
608
609
|
translation: {
|
|
610
|
+
"field.geolocation": "Geolocation",
|
|
609
611
|
"time.minute":"Minute",
|
|
610
612
|
"time.hour":"Hour",
|
|
611
613
|
"time.day":"Day",
|
|
@@ -2084,6 +2086,7 @@ export const websiteTranslations = {
|
|
|
2084
2086
|
},
|
|
2085
2087
|
es: {
|
|
2086
2088
|
translation: {
|
|
2089
|
+
"field.geolocation": "Geolocalización",
|
|
2087
2090
|
"time.minute": "Minuto",
|
|
2088
2091
|
"time.hour": "Hora",
|
|
2089
2092
|
"time.day": "Día",
|
|
@@ -3564,6 +3567,7 @@ export const websiteTranslations = {
|
|
|
3564
3567
|
},
|
|
3565
3568
|
pt: {
|
|
3566
3569
|
translation: {
|
|
3570
|
+
"field.geolocation": "Geolocalização",
|
|
3567
3571
|
"time.minute":"Minuto",
|
|
3568
3572
|
"time.hour":"Hora",
|
|
3569
3573
|
"time.day":"Dia",
|
|
@@ -5039,6 +5043,7 @@ export const websiteTranslations = {
|
|
|
5039
5043
|
},
|
|
5040
5044
|
de: {
|
|
5041
5045
|
translation: {
|
|
5046
|
+
"field.geolocation": "Geolokalisierung",
|
|
5042
5047
|
"time.minute":"Minute",
|
|
5043
5048
|
"time.hour":"Stunde",
|
|
5044
5049
|
"time.day":"Tag",
|
|
@@ -6499,6 +6504,7 @@ export const websiteTranslations = {
|
|
|
6499
6504
|
},
|
|
6500
6505
|
it: {
|
|
6501
6506
|
translation: {
|
|
6507
|
+
"field.geolocation": "Geolocalizzazione",
|
|
6502
6508
|
"time.minute":"Minuto",
|
|
6503
6509
|
"time.hour":"Ora",
|
|
6504
6510
|
"time.day":"Giorno",
|
|
@@ -7972,6 +7978,7 @@ export const websiteTranslations = {
|
|
|
7972
7978
|
},
|
|
7973
7979
|
cs: {
|
|
7974
7980
|
translation: {
|
|
7981
|
+
"field.geolocation": "Geolokace",
|
|
7975
7982
|
"time.minute":"Minuta",
|
|
7976
7983
|
"time.hour":"Hodina",
|
|
7977
7984
|
"time.day":"Den",
|
|
@@ -9439,6 +9446,7 @@ export const websiteTranslations = {
|
|
|
9439
9446
|
},
|
|
9440
9447
|
ru: {
|
|
9441
9448
|
translation: {
|
|
9449
|
+
"field.geolocation": "Геолокация",
|
|
9442
9450
|
"time.minute":"Минута",
|
|
9443
9451
|
"time.hour":"Час",
|
|
9444
9452
|
"time.day":"День",
|
|
@@ -10918,6 +10926,7 @@ export const websiteTranslations = {
|
|
|
10918
10926
|
},
|
|
10919
10927
|
ar: {
|
|
10920
10928
|
translation: {
|
|
10929
|
+
"field.geolocation": "تحديد الموقع الجغرافي",
|
|
10921
10930
|
"الدقيقة الزمنية": "الدقيقة", "الساعة الزمنية": "الساعة", "اليوم الزمني": "اليوم", "الأسبوع الزمني": "الأسبوع", "الشهر الزمني": "الشهر", "السنة الزمنية": "السنة",
|
|
10922
10931
|
"packs.myPack": "حزمتي",
|
|
10923
10932
|
"packs.public": "عامة",
|
|
@@ -12407,6 +12416,7 @@ export const websiteTranslations = {
|
|
|
12407
12416
|
},
|
|
12408
12417
|
sv: {
|
|
12409
12418
|
translation: {
|
|
12419
|
+
"field.geolocation": "Geolocation",
|
|
12410
12420
|
"time.minute":"Minut",
|
|
12411
12421
|
"time.hour":"Timme",
|
|
12412
12422
|
"time.day":"Dag",
|
|
@@ -13874,6 +13884,7 @@ export const websiteTranslations = {
|
|
|
13874
13884
|
},
|
|
13875
13885
|
el: {
|
|
13876
13886
|
translation: {
|
|
13887
|
+
"field.geolocation": "Γεωγραφική τοποθεσία",
|
|
13877
13888
|
"time.minute":"Λεπτό",
|
|
13878
13889
|
"time.hour":"Ώρα",
|
|
13879
13890
|
"time.day":"Ημέρα",
|
|
@@ -15350,6 +15361,7 @@ export const websiteTranslations = {
|
|
|
15350
15361
|
},
|
|
15351
15362
|
fa: {
|
|
15352
15363
|
"translation": {
|
|
15364
|
+
"field.geolocation": "موقعیت جغرافیایی",
|
|
15353
15365
|
"time.minute":"دقیقه",
|
|
15354
15366
|
"time.hour":"ساعت",
|
|
15355
15367
|
"time.day":"روز",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "data-primals-engine",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "data-primals-engine is a package responsible from handling large amount of data using MongoDB in a practical and performant way. It can also get workflow models working (for automation), and fully supports internationalisation. It also has integrated AI assistant.",
|
|
5
5
|
"main": "src/engine.js",
|
|
6
6
|
"type": "module",
|
|
@@ -101,6 +101,7 @@
|
|
|
101
101
|
"stripe": "^18.4.0",
|
|
102
102
|
"swagger-ui-express": "^5.0.1",
|
|
103
103
|
"tar": "^7.4.3",
|
|
104
|
+
"tinycolor2": "^1.6.0",
|
|
104
105
|
"uniqid": "^5.4.0",
|
|
105
106
|
"vitest": "^3.2.4",
|
|
106
107
|
"yaml": "^2.8.0"
|
package/perf/README.md
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
## Performance Test Report 1
|
|
2
|
+
|
|
3
|
+
### Test Environment
|
|
4
|
+
- **Machine**: Laptop Intel Core i7 Lenovo Legion
|
|
5
|
+
- **Concurrency**: Ramp-up to 50 virtual users/sec
|
|
6
|
+
- Runs on `perf-shot-search.yml` ,that simulates user basic activity on API
|
|
7
|
+
|
|
8
|
+
### Summary of Results
|
|
9
|
+
|
|
10
|
+
| Metric | Value |
|
|
11
|
+
|----------------------------|------------|
|
|
12
|
+
| Total HTTP Requests | 1,475 |
|
|
13
|
+
| Successful Responses (2xx) | 1,475 |
|
|
14
|
+
| Average Request Rate | 19/sec |
|
|
15
|
+
| Data Downloaded | 13.5 MB |
|
|
16
|
+
| Failed Scenarios | 0 |
|
|
17
|
+
|
|
18
|
+
### Response Time Analysis (in milliseconds)
|
|
19
|
+
|
|
20
|
+
| Percentile | Response Time (ms) |
|
|
21
|
+
|------------|--------------------|
|
|
22
|
+
| min | 8 |
|
|
23
|
+
| median | **36.2** |
|
|
24
|
+
| mean | 253.9 |
|
|
25
|
+
| p95 | 561.2 |
|
|
26
|
+
| p99 | 608 |
|
|
27
|
+
| max | 805 |
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
### Analysis & Recommendations
|
|
31
|
+
|
|
32
|
+
* **Outstanding Performance & Stability**: The final test results are excellent. The server handled **1,475 requests with 0 failures**, demonstrating exceptional stability under sustained load.
|
|
33
|
+
|
|
34
|
+
* **Bottleneck Resolved**:
|
|
35
|
+
* The **median response time is now just 36.2 milliseconds**. This is a dramatic improvement and indicates that the core API operations are extremely fast for the majority of users.
|
|
36
|
+
* The **p95 (561.2 ms)** and **p99 (608 ms)** response times remain well under one second. This shows that the application performs consistently, with very few outliers, even during peak load phases.
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
## Performance Test Report 2
|
|
41
|
+
|
|
42
|
+
### Test Environment
|
|
43
|
+
- **Machine**: Laptop Intel Core i7 Lenovo Legion
|
|
44
|
+
- **Scenario**: `perf-shot-hardwork.yml` (Full user journey including model creation, data import, and pack installation)
|
|
45
|
+
- **Concurrency**: Sustained load of 1 virtual user/sec for 60 seconds.
|
|
46
|
+
|
|
47
|
+
### Summary of Results
|
|
48
|
+
|
|
49
|
+
| Metric | Value |
|
|
50
|
+
|---|---|
|
|
51
|
+
| Total HTTP Requests | 480 |
|
|
52
|
+
| Successful Responses (2xx) | 480 |
|
|
53
|
+
| Average Request Rate | 4/sec |
|
|
54
|
+
| Data Downloaded | 0.88 MB |
|
|
55
|
+
| **Failed Scenarios** | **0** |
|
|
56
|
+
|
|
57
|
+
### Response Time Analysis (in milliseconds)
|
|
58
|
+
|
|
59
|
+
| Percentile | Response Time (ms) |
|
|
60
|
+
|------------|--------------------|
|
|
61
|
+
| min | 7 |
|
|
62
|
+
| median | **15** |
|
|
63
|
+
| mean | 96.1 |
|
|
64
|
+
| p95 | 368.8 |
|
|
65
|
+
| p99 | 407.5 |
|
|
66
|
+
| max | 474 |
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
### Analysis & Recommendations
|
|
70
|
+
|
|
71
|
+
* **Total Success & Stability**: The test is a complete success. With a 100% success rate (**0 failed scenarios** and **0 request errors**), it confirms that the previous fixes for race conditions and file upload issues are effective and robust.
|
|
72
|
+
|
|
73
|
+
* **Outstanding Performance**: The API's responsiveness is excellent.
|
|
74
|
+
* A **median response time of just 15 milliseconds** shows that the core operations are extremely fast for the majority of users.
|
|
75
|
+
* The **p99 is 407.5 ms**, meaning 99% of requests, even within a complex multi-step scenario, completed in well under half a second. This demonstrates consistent and reliable performance.
|
|
76
|
+
|
|
77
|
+
* **Conclusion**: The application is perfectly stable and highly performant under the tested load. The entire user journey, including complex operations, is handled efficiently. This provides a solid baseline for future, more intensive load tests.
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
## Performance Test Report 3 (Scaling)
|
|
81
|
+
|
|
82
|
+
### Test Environment
|
|
83
|
+
- **Machine**: Laptop Intel Core i7 Lenovo Legion
|
|
84
|
+
- **Scenario**: `perf-shot-hardwork.yml` (Full user journey)
|
|
85
|
+
- **Concurrency**: Ramped load from 1 to 2 virtual users/sec for 60 seconds.
|
|
86
|
+
|
|
87
|
+
### Summary of Results
|
|
88
|
+
|
|
89
|
+
| Metric | Value |
|
|
90
|
+
|---|---|
|
|
91
|
+
| Total HTTP Requests | 720 |
|
|
92
|
+
| Successful Responses (2xx) | 720 |
|
|
93
|
+
| Average Request Rate | 9/sec |
|
|
94
|
+
| **Failed Scenarios** | **0** |
|
|
95
|
+
|
|
96
|
+
### Response Time Analysis (in milliseconds)
|
|
97
|
+
|
|
98
|
+
| Percentile | Response Time (ms) |
|
|
99
|
+
|------------|--------------------|
|
|
100
|
+
| min | 8 |
|
|
101
|
+
| median | **32.1** |
|
|
102
|
+
| mean | 141.4 |
|
|
103
|
+
| p95 | 772.9 |
|
|
104
|
+
| p99 | 854.2 |
|
|
105
|
+
| max | 922 |
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
### Analysis & Recommendations
|
|
109
|
+
|
|
110
|
+
* **Excellent Linear Scaling**: This test confirms the application's ability to scale gracefully. When doubling the load from the previous test (1 user/sec to 2 users/sec), the median and p95 response times also roughly doubled. This linear behavior is ideal and indicates a robust architecture without major bottlenecks.
|
|
111
|
+
* **Continued Stability**: With **0 failed scenarios**, the application proves its stability even as concurrency increases.
|
|
112
|
+
* **Conclusion**: The application is ready for higher loads. The performance degradation is predictable and linear, which is a sign of a very healthy system.
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
## Performance Test Report 4 (Bottleneck Identification)
|
|
116
|
+
|
|
117
|
+
### Test Environment
|
|
118
|
+
- **Machine**: Laptop Intel Core i7 Lenovo Legion
|
|
119
|
+
- **Scenario**: `perf-shot-hardwork.yml` (Full user journey)
|
|
120
|
+
- **Concurrency**: Ramped load from 1 to 4 virtual users/sec for 60 seconds.
|
|
121
|
+
|
|
122
|
+
### Summary of Results
|
|
123
|
+
|
|
124
|
+
| Metric | Value |
|
|
125
|
+
|---|---|
|
|
126
|
+
| Total HTTP Requests | 1,200 |
|
|
127
|
+
| Successful Responses (2xx) | 1,200 |
|
|
128
|
+
| Average Request Rate | 12/sec |
|
|
129
|
+
| **Failed Scenarios** | **0** |
|
|
130
|
+
|
|
131
|
+
### Response Time Analysis (in milliseconds)
|
|
132
|
+
|
|
133
|
+
| Percentile | Response Time (ms) |
|
|
134
|
+
|------------|--------------------|
|
|
135
|
+
| min | 3 |
|
|
136
|
+
| median | **70.1** |
|
|
137
|
+
| mean | 483.9 |
|
|
138
|
+
| p95 | **2893.5** |
|
|
139
|
+
| p99 | **8024.5** |
|
|
140
|
+
| max | 8348 |
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
### Analysis & Recommendations
|
|
144
|
+
|
|
145
|
+
* **Bottleneck Identified**: This test successfully identified the application's first major performance bottleneck. While stability remains perfect (**0 failures**), the response times show non-linear degradation. The **p99 jumping to over 8 seconds** indicates that the system is struggling under this write-heavy load.
|
|
146
|
+
* **Database Contention**: The cause is almost certainly database contention (MongoDB). The scenario involves concurrent model creation, data import, and pack installation, which are highly resource-intensive operations.
|
|
147
|
+
* **Next Steps**: The immediate next step is to run a detailed analysis to pinpoint the exact slow queries. Monitoring server CPU/Disk I/O during the test and using MongoDB's profiler will be essential for optimization.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import {uuidv4} from "../src/core.js";
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
export function setScenarioVariables(context, events, done) {
|
|
5
|
+
// context.vars is where we store variables for the virtual user's session
|
|
6
|
+
context.vars.vuId = uuidv4().replace(/-/g, '');
|
|
7
|
+
context.vars.demoUser = `perf${Math.floor(Math.random() * 9000000) + 1000000}`;
|
|
8
|
+
return done();
|
|
9
|
+
}
|
|
10
|
+
// Hook to name requests to aggregate metrics by endpoint
|
|
11
|
+
export function metricsByEndpoint_beforeRequest(requestParams, context, ee, next) {
|
|
12
|
+
// Remove query parameters for the metric name
|
|
13
|
+
const urlPath = requestParams.url.split('?')[0];
|
|
14
|
+
|
|
15
|
+
// Replace unique IDs (ObjectId or UUID) in the URL with a placeholder for aggregation
|
|
16
|
+
// Example: /api/data/60d0fe4f5311236168a109ca -> /api/data/:id
|
|
17
|
+
const name = urlPath.replace(/([a-fA-F0-9]{24}|[a-fA-F0-9-]{36})/g, ':id');
|
|
18
|
+
|
|
19
|
+
// Emit an internal Artillery event to name the request
|
|
20
|
+
ee.emit('request:set_name', name);
|
|
21
|
+
|
|
22
|
+
return next();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Hook pour logger les erreurs de manière détaillée
|
|
26
|
+
export function metricsByEndpoint_afterResponse(requestParams, response, context, ee, next) {
|
|
27
|
+
// On vérifie si le statut est un code d'erreur (4xx ou 5xx)
|
|
28
|
+
if (response.statusCode >= 400) {
|
|
29
|
+
console.error(`\n[ERREUR] Requête ${requestParams.method} ${requestParams.url} a échoué avec le statut ${response.statusCode}`);
|
|
30
|
+
// On log le corps de la réponse, qui contient souvent le message d'erreur du serveur
|
|
31
|
+
if (response.body) {
|
|
32
|
+
console.error(` -> Réponse: ${response.body}\n`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// On passe à la suite du scénario
|
|
36
|
+
return next();
|
|
37
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
config:
|
|
2
|
+
# L'URL de base de votre API
|
|
3
|
+
processor: "./artillery-hooks.js" # Indique à Artillery d'utiliser notre fichier de hooks
|
|
4
|
+
target: "http://localhost:7633"
|
|
5
|
+
# Configuration des phases de test
|
|
6
|
+
phases:
|
|
7
|
+
# Phase 1: Montée en charge progressive pendant 30 secondes
|
|
8
|
+
- duration: 60
|
|
9
|
+
arrivalRate: 1 # 5 nouveaux utilisateurs virtuels par seconde
|
|
10
|
+
rampTo: 1 # pour atteindre 20 nouveaux utilisateurs virtuels par seconde à la fin de la phase
|
|
11
|
+
name: "Ramp-up"
|
|
12
|
+
# Phase 2: Maintien de la charge maximale pendant 60 secondes
|
|
13
|
+
# Paramètres par défaut pour toutes les requêtes HTTP
|
|
14
|
+
defaults:
|
|
15
|
+
headers:
|
|
16
|
+
# Utilisation du token statique de l'utilisateur 'demo'
|
|
17
|
+
Authorization: "Bearer demotoken"
|
|
18
|
+
Content-Type: 'application/json'
|
|
19
|
+
beforeRequest: "metricsByEndpoint_beforeRequest" # Groups metrics by endpoint
|
|
20
|
+
afterResponse: "metricsByEndpoint_afterResponse" # Logs errors
|
|
21
|
+
scenarios:
|
|
22
|
+
- name: "Parcours Utilisateur Complet"
|
|
23
|
+
flow:
|
|
24
|
+
# Sets unique variables for EACH scenario run
|
|
25
|
+
- function: "setScenarioVariables"
|
|
26
|
+
- log: "--- Démarrage du scénario utilisateur 'developer' ---"
|
|
27
|
+
- post:
|
|
28
|
+
url: "/api/demo/initialize?_user={{demoUser}}"
|
|
29
|
+
json:
|
|
30
|
+
profile: "developer"
|
|
31
|
+
packs:
|
|
32
|
+
- "Multilingual starter pack"
|
|
33
|
+
- "Website Starter Pack"
|
|
34
|
+
- log: "--- Searching in translation table by lang relation ---"
|
|
35
|
+
- post:
|
|
36
|
+
url: "/api/data/search?_user={{demoUser}}"
|
|
37
|
+
json:
|
|
38
|
+
model: "translation"
|
|
39
|
+
filter: { "lang": { "$find": { "code": "en" } } }
|
|
40
|
+
limit: 250
|
|
41
|
+
- log: "--- Étape 2: Ajout d'une nouvelle donnée et capture de son ID ---"
|
|
42
|
+
- post:
|
|
43
|
+
url: "/api/data?_user={{demoUser}}"
|
|
44
|
+
json:
|
|
45
|
+
model: "translation"
|
|
46
|
+
data:
|
|
47
|
+
key: "perf.test.greeting.{{ $uuid }}"
|
|
48
|
+
value: "Hello from performance test!"
|
|
49
|
+
lang: { "$find": { "code": "en" } } # Syntaxe $find corrigée
|
|
50
|
+
capture:
|
|
51
|
+
- json: "$.insertedIds[0]"
|
|
52
|
+
as: "insertedId"
|
|
53
|
+
- log: "--- Étape 3: Import de données (nécessite un modèle et un fichier) ---"
|
|
54
|
+
# 3a. Création d'un modèle pour l'import (tolère l'erreur si le modèle existe déjà)
|
|
55
|
+
- post:
|
|
56
|
+
url: "/api/model?_user={{demoUser}}"
|
|
57
|
+
json:
|
|
58
|
+
name: "perf_import_items_{{ vuId }}" # Unique model name to avoid race conditions
|
|
59
|
+
description: "import model for perf test"
|
|
60
|
+
fields:
|
|
61
|
+
- { name: "name", type: "string" }
|
|
62
|
+
- { name: "value", type: "number" }
|
|
63
|
+
expect:
|
|
64
|
+
- statusCode: 201 # Succès de la création
|
|
65
|
+
- statusCode: 400 # Tolère l'erreur "le modèle existe déjà" pour rendre le test ré-exécutable
|
|
66
|
+
- log: "--- Étape 4: Installation d'un pack de données ---"
|
|
67
|
+
- post:
|
|
68
|
+
url: "/api/packs/install?_user={{demoUser}}&lang=en"
|
|
69
|
+
json:
|
|
70
|
+
packData:
|
|
71
|
+
name: "Perf Test Pack {{ $uuid }}"
|
|
72
|
+
description: "A pack for performance testing."
|
|
73
|
+
models: [{ name: "perf_pack_model_{{ vuId }}", description: "", fields: [{ name: "pack_field", type: "string" }] }] # Unique model name
|
|
74
|
+
data: { all: { "perf_pack_model_{{ vuId }}": [{ "pack_field": "Hello from a pack!" }] } }
|
|
75
|
+
- log: "--- Étape 5: Suppression des données ---"
|
|
76
|
+
- delete:
|
|
77
|
+
url: "/api/model?name=perf_import_items_{{ vuId }}&_user={{demoUser}}" # Delete the unique model
|
|
78
|
+
- delete:
|
|
79
|
+
url: "/api/model?name=perf_pack_model_{{ vuId }}&_user={{demoUser}}" # Delete the unique model
|
|
80
|
+
- delete:
|
|
81
|
+
url: "/api/data/{{ insertedId }}?_user={{demoUser}}"
|
|
82
|
+
json:
|
|
83
|
+
model: "translation"
|
|
84
|
+
ifTrue: "{{ insertedId !== undefined }}" # Ne s'exécute que si l'ID a été capturé
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
config:
|
|
2
|
+
# L'URL de base de votre API
|
|
3
|
+
target: "http://localhost:7633"
|
|
4
|
+
# Configuration des phases de test
|
|
5
|
+
phases:
|
|
6
|
+
# Phase 1: Montée en charge progressive pendant 60 secondes
|
|
7
|
+
- duration: 10
|
|
8
|
+
arrivalRate: 5 # 5 nouveaux utilisateurs virtuels par seconde
|
|
9
|
+
rampTo: 50 # pour atteindre 50 nouveaux utilisateurs par seconde à la fin de la phase
|
|
10
|
+
name: "Ramp-up"
|
|
11
|
+
# Phase 2: Maintien de la charge maximale pendant 120 secondes
|
|
12
|
+
- duration: 120
|
|
13
|
+
arrivalRate: 10 # 50 nouveaux utilisateurs virtuels par seconde
|
|
14
|
+
name: "Sustained load"
|
|
15
|
+
# Paramètres par défaut pour toutes les requêtes HTTP
|
|
16
|
+
defaults:
|
|
17
|
+
headers:
|
|
18
|
+
# Utilisation du token statique de l'utilisateur 'demo'
|
|
19
|
+
Authorization: "Bearer demotoken"
|
|
20
|
+
Content-Type: 'application/json'
|
|
21
|
+
|
|
22
|
+
scenarios:
|
|
23
|
+
- name: "Stress Test - Recherche de données"
|
|
24
|
+
flow: # Envoi d'une requête POST à l'endpoint de recherche
|
|
25
|
+
- post:
|
|
26
|
+
url: "/api/data/search?_user=demo"
|
|
27
|
+
# Le corps de la requête en JSON
|
|
28
|
+
json:
|
|
29
|
+
# Le nom du modèle sur lequel effectuer la recherche
|
|
30
|
+
model: "translation"
|
|
31
|
+
filter: { "lang": { "$find": { "code": "en" } } }
|
|
32
|
+
limit: 250
|
|
33
|
+
- name: "Stress Test - Insertion de données"
|
|
34
|
+
flow: # Envoi d'une requête POST à l'endpoint de recherche
|
|
35
|
+
- post:
|
|
36
|
+
url: "/api/data?_user=demo"
|
|
37
|
+
# Le corps de la requête en JSON
|
|
38
|
+
json:
|
|
39
|
+
# Le nom du modèle sur lequel effectuer la recherche
|
|
40
|
+
model: "translation"
|
|
41
|
+
# Le filtre de recherche (ici, vide pour récupérer tous les documents)
|
|
42
|
+
data: {
|
|
43
|
+
"key": "{{$randomNumber(1,10000)}}",
|
|
44
|
+
"value": "{{$randomNumber(1,10000)}}",
|
|
45
|
+
}
|
package/perf/setup.yml
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
config:
|
|
2
|
+
target: "http://localhost:7633"
|
|
3
|
+
phases:
|
|
4
|
+
# Exécute ce scénario une seule fois avec un seul utilisateur virtuel
|
|
5
|
+
- duration: 1
|
|
6
|
+
arrivalCount: 1
|
|
7
|
+
defaults:
|
|
8
|
+
headers:
|
|
9
|
+
# Utilise le token statique de l'utilisateur 'demo' pour l'installation
|
|
10
|
+
Authorization: "Bearer demotoken"
|
|
11
|
+
Content-Type: 'application/json'
|
|
12
|
+
|
|
13
|
+
scenarios:
|
|
14
|
+
- name: "Installation unique des packs de données"
|
|
15
|
+
flow:
|
|
16
|
+
- log: "Installation du 'Website Starter Pack'..."
|
|
17
|
+
- post:
|
|
18
|
+
url: "/api/packs/install?_user=demo"
|
|
19
|
+
json:
|
|
20
|
+
packName: "Website Starter Pack"
|
|
21
|
+
- log: "Installation du 'Multilingual starter pack'..."
|
|
22
|
+
- post:
|
|
23
|
+
url: "/api/packs/install?_user=demo"
|
|
24
|
+
json:
|
|
25
|
+
packName: "Multilingual starter pack"
|
|
26
|
+
- log: "Installation terminée."
|
package/server.js
CHANGED
|
@@ -29,7 +29,7 @@ if (process.argv.length === 3) {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
|
|
32
|
-
const realPort = process.env.PORT || port;
|
|
32
|
+
const realPort = Config.Get('port', process.env.PORT || port);
|
|
33
33
|
engine.start(realPort, async (r) => {
|
|
34
34
|
const logger = engine.getComponent(Logger);
|
|
35
35
|
console.log("Server started on port " + realPort);
|