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.
Files changed (59) hide show
  1. package/README.md +878 -856
  2. package/client/package-lock.json +82 -0
  3. package/client/package.json +2 -0
  4. package/client/src/App.jsx +1 -1
  5. package/client/src/App.scss +25 -7
  6. package/client/src/AssistantChat.scss +3 -2
  7. package/client/src/ConditionBuilder.jsx +1 -1
  8. package/client/src/ConditionBuilder2.jsx +2 -1
  9. package/client/src/DashboardView.jsx +569 -569
  10. package/client/src/DataEditor.jsx +376 -368
  11. package/client/src/DataLayout.jsx +4 -10
  12. package/client/src/DataTable.jsx +858 -817
  13. package/client/src/Field.jsx +1825 -1784
  14. package/client/src/FlexDataRenderer.jsx +2 -0
  15. package/client/src/FlexTreeUtils.js +1 -1
  16. package/client/src/GeolocationField.jsx +94 -0
  17. package/client/src/KPIDialog.jsx +11 -1
  18. package/client/src/ModelCreator.jsx +1 -2
  19. package/client/src/ModelCreatorField.jsx +24 -27
  20. package/client/src/ModelList.jsx +1 -1
  21. package/client/src/RelationField.jsx +2 -2
  22. package/client/src/constants.js +3 -3
  23. package/client/src/filter.js +0 -155
  24. package/client/src/hooks/useTutorials.jsx +62 -65
  25. package/client/src/translations.js +14 -2
  26. package/package.json +2 -1
  27. package/perf/README.md +147 -0
  28. package/perf/artillery-hooks.js +37 -0
  29. package/perf/perf-shot-hardwork.yml +84 -0
  30. package/perf/perf-shot-search.yml +45 -0
  31. package/perf/setup.yml +26 -0
  32. package/server.js +1 -1
  33. package/src/constants.js +264 -31
  34. package/src/core.js +15 -1
  35. package/src/data.js +1 -1
  36. package/src/defaultModels.js +1544 -1540
  37. package/src/email.js +5 -2
  38. package/src/engine.js +10 -3
  39. package/src/filter.js +274 -260
  40. package/src/i18n.js +187 -177
  41. package/src/modules/assistant/assistant.js +3 -1
  42. package/src/modules/bucket.js +12 -15
  43. package/src/modules/data/data.backup.js +11 -8
  44. package/src/modules/data/data.core.js +2 -1
  45. package/src/modules/data/data.js +6 -3
  46. package/src/modules/data/data.operations.js +610 -168
  47. package/src/modules/data/data.routes.js +1821 -1785
  48. package/src/modules/data/data.scheduling.js +2 -1
  49. package/src/modules/data/data.validation.js +7 -1
  50. package/src/modules/file.js +4 -2
  51. package/src/modules/user.js +4 -1
  52. package/src/modules/workflow.js +9 -10
  53. package/src/openai.jobs.js +2 -0
  54. package/src/packs.js +22 -5
  55. package/src/providers.js +22 -7
  56. package/swagger-en.yml +133 -0
  57. package/test/data.integration.test.js +1060 -981
  58. package/test/import_export.integration.test.js +1 -1
  59. 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
- // Refs pour gérer la file d'attente des vérifications
24
- const isCheckingRef = useRef(false);
25
- const isCheckQueuedRef = useRef(false);
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
- if (isCheckingRef.current) {
125
- console.log('[Tutoriels] Vérification déjà en cours, mise en file d\'attente.');
126
- isCheckQueuedRef.current = true; // Mettre en attente
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
- isCheckingRef.current = true;
132
- console.log('[Tutoriels] Démarrage de la vérification en chaîne...');
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
- let currentActiveState = me.activeTutorial;
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
- while (true) {
138
- const tutorial = tutorials.find(t => t._id === currentActiveState.id);
139
- if (!tutorial) break;
150
+ const { isCompleted } = await checkCompletionCondition(stageConfig.completionCondition);
140
151
 
141
- const stageConfig = tutorial.stages.find(s => s.stage === currentActiveState.stage);
142
- if (!stageConfig) {
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 { isCompleted } = await checkCompletionCondition(stageConfig.completionCondition);
155
+ const titleKey = `tutorial.${tutorial.id}.stage.${currentStageNumber}.name`;
156
+ const descriptionKey = `tutorial.${tutorial.id}.stage.${currentStageNumber}.description`;
151
157
 
152
- if (isCompleted) {
153
- console.log(`[Tutoriels] Étape ${currentActiveState.stage} complétée, passage à la suivante.`);
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
- // Clés de traduction structurées et robustes
156
- const titleKey = `tutorial.${tutorial.id}.stage.${currentActiveState.stage}.name`;
157
- const descriptionKey = `tutorial.${tutorial.id}.stage.${currentActiveState.stage}.description`;
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
- addNotification({
160
- title: t(titleKey, stageConfig.name),
161
- message: t(descriptionKey, stageConfig.description),
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
- console.log(`[Tutoriels] Arrêt à l'étape ${currentActiveState.stage} (non complétée).`);
167
- if (currentActiveState.stage !== me.activeTutorial.stage) {
168
- updateActiveTutorial(currentActiveState);
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 en chaîne :", error);
181
+ console.error("[Tutoriels] Erreur lors de la vérification de l'étape :", error);
176
182
  } finally {
177
- isCheckingRef.current = false;
178
- console.log('[Tutoriels] Vérification en chaîne terminée, verrou libéré.');
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
- }, [me?.activeTutorial]);
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.4.2",
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);