Copilot commented on code in PR #775:
URL: https://github.com/apache/unomi/pull/775#discussion_r3425974036


##########
rest/src/main/java/org/apache/unomi/rest/endpoints/ContextJsonEndpoint.java:
##########
@@ -191,9 +213,17 @@ public ContextResponse contextJSONAsPost(ContextRequest 
contextRequest,
                 contextResponse.setSessionId(sessionId);
             }
 
+            // Add tracing information if requested
+            if (explain) {
+                
contextResponse.setRequestTracing(tracerService.getTraceNode());
+                tracerService.getCurrentTracer().endOperation(null, "Context 
request processed successfully");
+            }

Review Comment:
   When `explain=true`, the root tracing operation is ended *after* the trace 
tree is captured into the response. This can return a `TraceNode` without the 
final endTime/description/result. End the operation first, then attach 
`getTraceNode()` to the response.



##########
services/src/main/java/org/apache/unomi/services/impl/segments/SegmentServiceImpl.java:
##########
@@ -388,52 +391,71 @@ public void setSegmentDefinition(Segment segment) {
                 throw new BadSegmentConditionException();
             }
         } else {
+            // Start validation operation in tracer
+            if (tracerService != null) {
+                RequestTracer tracer = tracerService.getCurrentTracer();
+                if (tracer != null && tracer.isEnabled()) {
+                    tracer.startOperation("segment-condition-validation", 
"Validating segment condition: " + segment.getItemId(), segment.getCondition());
+                }
+            }
+
             if (segment.getMetadata().isEnabled()) {
                 TypeResolutionService typeResolutionService = 
getTypeResolutionService();
                 if (typeResolutionService != null) {
                     typeResolutionService.resolveCondition("segments", 
segment, segment.getCondition(), "segment " + segment.getItemId());
                 }
+            }
 
-                if 
(!persistenceService.isValidCondition(segment.getCondition(), new 
Profile(VALIDATION_PROFILE_ID))) {
-                    throw new BadSegmentConditionException();
-                }
+            // Validate condition (skips parameters with references/scripts)
+            // Validation service will auto-resolve types if needed
+            List<ValidationError> validationErrors = 
definitionsService.getConditionValidationService().validate(segment.getCondition());
 
-                if (definitionsService.getConditionValidationService() != 
null) {
-                    List<ValidationError> validationErrors = 
definitionsService.getConditionValidationService().validate(segment.getCondition());
+            // Add validation info to tracer
+            if (tracerService != null) {
+                RequestTracer tracer = tracerService.getCurrentTracer();
+                if (tracer != null && tracer.isEnabled()) {
+                    tracer.addValidationInfo(validationErrors, 
"segment-condition-validation");
+                    tracer.endOperation(!validationErrors.isEmpty(), 
String.format("Segment validation completed with %d errors", 
validationErrors.size()));
+                }

Review Comment:
   The tracer operation result passed to `endOperation(...)` is inverted: 
`!validationErrors.isEmpty()` reports success when there are errors. This makes 
trace output misleading for explain/debug.



##########
rest/src/main/java/org/apache/unomi/rest/endpoints/EventsCollectorEndpoint.java:
##########
@@ -122,9 +139,17 @@ private EventCollectorResponse 
doEvent(EventsCollectorRequest eventsCollectorReq
 
             EventCollectorResponse response = new 
EventCollectorResponse(eventsRequestContext.getChanges());
 
+            // Add tracing information if requested
+            if (explain) {
+                response.setRequestTracing(tracerService.getTraceNode());
+                tracerService.getCurrentTracer().endOperation(null, "Event 
collection request processed successfully");
+            }

Review Comment:
   When `explain=true`, the root tracing operation is ended *after* the trace 
tree is captured into the response. This means the returned `TraceNode` may 
miss the final description/result/endTime from `endOperation()`. End the 
operation first, then call `getTraceNode()` and attach it to the response.



##########
services/src/main/java/org/apache/unomi/services/impl/rules/RulesServiceImpl.java:
##########
@@ -677,10 +683,29 @@ protected void setRule(Rule rule, boolean 
allowInvalidRules) {
             }
         }
 
-
         if (rule.getCondition() != null) {
+            // Start validation operation in tracer
+            if (tracerService != null) {
+                RequestTracer tracer = tracerService.getCurrentTracer();
+                if (tracer != null && tracer.isEnabled()) {
+                    tracer.startOperation("rule-condition-validation", 
"Validating rule condition: " + rule.getItemId(), rule.getCondition());
+                }
+            }
+
+            // Validate condition (skips parameters with references/scripts)
+            // Validation service will auto-resolve types if needed
             List<ValidationError> validationErrors = 
definitionsService.getConditionValidationService().validate(rule.getCondition());
 
+            // Add validation info to tracer
+            if (tracerService != null) {
+                RequestTracer tracer = tracerService.getCurrentTracer();
+                if (tracer != null && tracer.isEnabled()) {
+                    tracer.addValidationInfo(validationErrors, 
"rule-condition-validation");
+                    tracer.endOperation(!validationErrors.isEmpty(), 
String.format("Rule validation completed with %d errors", 
validationErrors.size()));
+                }

Review Comment:
   The tracer operation result passed to `endOperation(...)` is inverted: 
`!validationErrors.isEmpty()` reports success when there are errors. This makes 
explain traces misleading.



##########
services/src/main/java/org/apache/unomi/services/impl/goals/GoalsServiceImpl.java:
##########
@@ -188,8 +198,119 @@ public void setGoal(Goal goal) {
             LOGGER.warn("Trying to save null goal, aborting...");
             return;
         }
-        ParserHelper.resolveConditionType(definitionsService, 
goal.getStartEvent(), "goal "+goal.getItemId()+" start event");
-        ParserHelper.resolveConditionType(definitionsService, 
goal.getTargetEvent(), "goal "+goal.getItemId()+" start event");
+        boolean isValid = true;
+        if (goal.getStartEvent() != null) {
+            // Resolve condition type (skips parameter resolution - happens 
on-demand in query builders/evaluators)
+            TypeResolutionService typeResolutionService = 
getTypeResolutionService();
+            boolean startValid = typeResolutionService != null && 
typeResolutionService.resolveCondition("goals", goal, goal.getStartEvent(), 
"goal "+goal.getItemId()+" start event");
+            isValid = isValid && startValid;
+            // Start validation operation in tracer for start event
+            if (tracerService != null) {
+                RequestTracer tracer = tracerService.getCurrentTracer();
+                if (tracer != null && tracer.isEnabled()) {
+                    tracer.startOperation("goal-start-event-validation", 
"Validating goal start event: " + goal.getItemId(), goal.getStartEvent());
+                }
+            }
+
+            // Validate condition (skips parameters with references/scripts)
+            // Validation service will auto-resolve types if needed
+            List<ValidationError> validationErrors = 
definitionsService.getConditionValidationService().validate(goal.getStartEvent());
+
+            // Add validation info to tracer
+            if (tracerService != null) {
+                RequestTracer tracer = tracerService.getCurrentTracer();
+                if (tracer != null && tracer.isEnabled()) {
+                    tracer.addValidationInfo(validationErrors, 
"goal-start-event-validation");
+                    tracer.endOperation(!validationErrors.isEmpty(), 
String.format("Goal start event validation completed with %d errors", 
validationErrors.size()));
+                }

Review Comment:
   The tracer operation result passed to `endOperation(...)` is inverted: 
`!validationErrors.isEmpty()` reports success when there are errors. This makes 
goal explain traces misleading.



##########
services/src/main/java/org/apache/unomi/services/impl/segments/SegmentServiceImpl.java:
##########
@@ -1072,7 +1101,7 @@ private void 
recalculatePastEventOccurrencesOnProfiles(Condition eventCondition,
 
         l.add(eventCondition);
 
-        Integer numberOfDays = 
PropertyHelper.getInteger(parentCondition.getParameter("numberOfDays"));
+        Integer numberOfDays = (Integer) 
parentCondition.getParameter("numberOfDays");

Review Comment:
   `numberOfDays` is cast to `Integer`, but it is sometimes provided as a 
numeric string (see existing integration tests). This cast will throw 
`ClassCastException` and break past-event segment recalculation. Use 
`PropertyHelper.getInteger(...)` coercion instead.



##########
plugins/baseplugin/src/main/java/org/apache/unomi/plugins/baseplugin/conditions/PropertyConditionEvaluator.java:
##########
@@ -239,9 +257,32 @@ public boolean eval(Condition condition, Item item, 
Map<String, Object> context,
                 actualValue = ConditionContextHelper.foldToASCII((String) 
actualValue);
             }
 
+            final Object finalActualValue = actualValue;
+            if (tracer != null) {
+                tracer.trace("Property value comparison: " + name + " " + op + 
" " + expectedValue,
+                    new HashMap<String, Object>() {{
+                        put("actualValue", finalActualValue);
+                        put("expectedValue", expectedValue);
+                        put("expectedValueInteger", expectedValueInteger);
+                        put("expectedValueDouble", expectedValueDouble);
+                        put("expectedValueDate", expectedValueDate);
+                        put("expectedValueDateExpr", expectedValueDateExpr);
+                    }});

Review Comment:
   This uses double-brace initialization (`new HashMap<>() {{ ... }}`), which 
creates an extra anonymous class and can retain references unexpectedly. It 
also adds allocation overhead inside a hot path (condition evaluation). Prefer 
building a `Map` normally and passing it to `tracer.trace(...)`.



##########
persistence-spi/src/main/java/org/apache/unomi/persistence/spi/conditions/ConditionContextHelper.java:
##########
@@ -204,17 +219,19 @@ private static Object parseParameterWithValidation(
      * @param value the value to resolve
      * @param scriptExecutor executor for script expressions
      * @param parameterDefs optional parameter definitions for validation
+     * @param tracerService optional tracer service for warnings
      * @param conditionTypeId condition type ID for context
      * @param resolutionChain set of parameter keys/scripts already being 
resolved (for cycle detection)
      * @param depth current resolution depth
-     * @return resolved value, or {@code RESOLUTION_ERROR} sentinel if cycle 
detected or max depth exceeded
+     * @return resolved value, or null if cycle detected or max depth exceeded
      */

Review Comment:
   The Javadoc says this method returns `null` when a cycle/max-depth is 
detected, but the implementation returns the `RESOLUTION_ERROR` sentinel. This 
mismatch can confuse future maintenance/debugging of parameter resolution.



##########
plugins/baseplugin/src/main/java/org/apache/unomi/plugins/baseplugin/actions/SetEventOccurenceCountAction.java:
##########
@@ -47,85 +46,118 @@ public void setPersistenceService(PersistenceService 
persistenceService) {
         this.persistenceService = persistenceService;
     }
 
+    public void setTracerService(TracerService tracerService) {
+        this.tracerService = tracerService;
+    }
+
     @Override
     public int execute(Action action, Event event) {
-        final Condition pastEventCondition = (Condition) 
action.getParameterValues().get("pastEventCondition");
-
-        Condition andCondition = new 
Condition(definitionsService.getConditionType("booleanCondition"));
-        andCondition.setParameter("operator", "and");
-        ArrayList<Condition> conditions = new ArrayList<Condition>();
-
-        Condition eventCondition = (Condition) 
pastEventCondition.getParameter("eventCondition");
-        definitionsService.resolveConditionType(eventCondition);
-        conditions.add(eventCondition);
+        RequestTracer tracer = null;
+        if (tracerService != null && tracerService.isTracingEnabled()) {
+            tracer = tracerService.getCurrentTracer();
+            tracer.startOperation("set-event-count",
+                "Setting event occurrence count", action);
+        }
 
-        Condition c = new 
Condition(definitionsService.getConditionType("eventPropertyCondition"));
-        c.setParameter("propertyName", "profileId");
-        c.setParameter("comparisonOperator", "equals");
-        c.setParameter("propertyValue", event.getProfileId());
-        conditions.add(c);
+        try {
+            final Condition pastEventCondition = (Condition) 
action.getParameterValues().get("pastEventCondition");
+            String generatedPropertyKey = (String) 
pastEventCondition.getParameter("generatedPropertyKey");
 
-        // may be current event is already persisted and indexed, in that case 
we filter it from the count to increment it manually at the end
-        Condition eventIdFilter = new 
Condition(definitionsService.getConditionType("eventPropertyCondition"));
-        eventIdFilter.setParameter("propertyName", "itemId");
-        eventIdFilter.setParameter("comparisonOperator", "notEquals");
-        eventIdFilter.setParameter("propertyValue", event.getItemId());
-        conditions.add(eventIdFilter);
+            if (tracer != null) {
+                tracer.trace("Processing event count", Map.of(
+                    "propertyKey", generatedPropertyKey,
+                    "eventId", event.getItemId()
+                ));
+            }
 
-        Integer numberOfDays = (Integer) 
pastEventCondition.getParameter("numberOfDays");
-        String fromDate = (String) pastEventCondition.getParameter("fromDate");
-        String toDate = (String) pastEventCondition.getParameter("toDate");
+            Condition andCondition = new 
Condition(definitionsService.getConditionType("booleanCondition"));
+            andCondition.setParameter("operator", "and");
+            ArrayList<Condition> conditions = new ArrayList<Condition>();
 
-        if (numberOfDays != null) {
-            Condition numberOfDaysCondition = new 
Condition(definitionsService.getConditionType("eventPropertyCondition"));
-            numberOfDaysCondition.setParameter("propertyName", "timeStamp");
-            numberOfDaysCondition.setParameter("comparisonOperator", 
"greaterThan");
-            numberOfDaysCondition.setParameter("propertyValueDateExpr", "now-" 
+ numberOfDays + "d");
-            conditions.add(numberOfDaysCondition);
-        }
-        if (fromDate != null)  {
-            Condition startDateCondition = new Condition();
-            
startDateCondition.setConditionType(definitionsService.getConditionType("eventPropertyCondition"));
-            startDateCondition.setParameter("propertyName", "timeStamp");
-            startDateCondition.setParameter("comparisonOperator", 
"greaterThanOrEqualTo");
-            startDateCondition.setParameter("propertyValueDate", fromDate);
-            conditions.add(startDateCondition);
-        }
-        if (toDate != null)  {
-            Condition endDateCondition = new Condition();
-            
endDateCondition.setConditionType(definitionsService.getConditionType("eventPropertyCondition"));
-            endDateCondition.setParameter("propertyName", "timeStamp");
-            endDateCondition.setParameter("comparisonOperator", 
"lessThanOrEqualTo");
-            endDateCondition.setParameter("propertyValueDate", toDate);
-            conditions.add(endDateCondition);
-        }
+            Condition eventCondition = (Condition) 
pastEventCondition.getParameter("eventCondition");
+            if (eventCondition != null) {
+                
definitionsService.getConditionValidationService().validate(eventCondition);
+            }
+            conditions.add(eventCondition);
+

Review Comment:
   `eventCondition` may be null, but it is unconditionally added to 
`subConditions`. This will later cause NPEs when the boolean condition is 
evaluated / translated into a query. If `eventCondition` is missing, this 
action should fail fast (or skip execution) instead of adding a null 
subcondition.



##########
extensions/groovy-actions/services/src/main/java/org/apache/unomi/groovy/actions/GroovyActionDispatcher.java:
##########
@@ -67,32 +73,54 @@ public void 
setActionExecutorDispatcher(ActionExecutorDispatcher actionExecutorD
         this.actionExecutorDispatcher = actionExecutorDispatcher;
     }
 
+    @Reference
+    public void setTracerService(TracerService tracerService) {
+        this.tracerService = tracerService;
+    }
+
     public String getPrefix() {
         return GROOVY_PREFIX;
     }
 
     public Integer execute(Action action, Event event, String actionName) {
-        Class<? extends Script> scriptClass = 
groovyActionsService.getCompiledScript(actionName);
-        if (scriptClass == null) {
-            LOGGER.warn("Couldn't find a Groovy action with name {}, action 
will not execute!", actionName);
-            return 0;
+        RequestTracer tracer = tracerService.getCurrentTracer();
+        if (!tracer.isEnabled()) {
+            tracer.setEnabled(true);
         }
-        
+

Review Comment:
   This dispatcher forcibly enables tracing (`tracer.setEnabled(true)`) even 
when tracing was not requested. Because tracers are ThreadLocal, this can 
unintentionally keep tracing enabled for later requests handled by the same 
thread. Tracing should be opt-in (only when `TracerService.isTracingEnabled()` 
is true) and not enabled from inside action dispatchers.



##########
plugins/baseplugin/src/main/java/org/apache/unomi/plugins/baseplugin/actions/SetEventOccurenceCountAction.java:
##########
@@ -47,85 +46,118 @@ public void setPersistenceService(PersistenceService 
persistenceService) {
         this.persistenceService = persistenceService;
     }
 
+    public void setTracerService(TracerService tracerService) {
+        this.tracerService = tracerService;
+    }
+
     @Override
     public int execute(Action action, Event event) {
-        final Condition pastEventCondition = (Condition) 
action.getParameterValues().get("pastEventCondition");
-
-        Condition andCondition = new 
Condition(definitionsService.getConditionType("booleanCondition"));
-        andCondition.setParameter("operator", "and");
-        ArrayList<Condition> conditions = new ArrayList<Condition>();
-
-        Condition eventCondition = (Condition) 
pastEventCondition.getParameter("eventCondition");
-        definitionsService.resolveConditionType(eventCondition);
-        conditions.add(eventCondition);
+        RequestTracer tracer = null;
+        if (tracerService != null && tracerService.isTracingEnabled()) {
+            tracer = tracerService.getCurrentTracer();
+            tracer.startOperation("set-event-count",
+                "Setting event occurrence count", action);
+        }
 
-        Condition c = new 
Condition(definitionsService.getConditionType("eventPropertyCondition"));
-        c.setParameter("propertyName", "profileId");
-        c.setParameter("comparisonOperator", "equals");
-        c.setParameter("propertyValue", event.getProfileId());
-        conditions.add(c);
+        try {
+            final Condition pastEventCondition = (Condition) 
action.getParameterValues().get("pastEventCondition");
+            String generatedPropertyKey = (String) 
pastEventCondition.getParameter("generatedPropertyKey");
 
-        // may be current event is already persisted and indexed, in that case 
we filter it from the count to increment it manually at the end
-        Condition eventIdFilter = new 
Condition(definitionsService.getConditionType("eventPropertyCondition"));
-        eventIdFilter.setParameter("propertyName", "itemId");
-        eventIdFilter.setParameter("comparisonOperator", "notEquals");
-        eventIdFilter.setParameter("propertyValue", event.getItemId());
-        conditions.add(eventIdFilter);
+            if (tracer != null) {
+                tracer.trace("Processing event count", Map.of(
+                    "propertyKey", generatedPropertyKey,
+                    "eventId", event.getItemId()
+                ));
+            }
 
-        Integer numberOfDays = (Integer) 
pastEventCondition.getParameter("numberOfDays");
-        String fromDate = (String) pastEventCondition.getParameter("fromDate");
-        String toDate = (String) pastEventCondition.getParameter("toDate");
+            Condition andCondition = new 
Condition(definitionsService.getConditionType("booleanCondition"));
+            andCondition.setParameter("operator", "and");
+            ArrayList<Condition> conditions = new ArrayList<Condition>();
 
-        if (numberOfDays != null) {
-            Condition numberOfDaysCondition = new 
Condition(definitionsService.getConditionType("eventPropertyCondition"));
-            numberOfDaysCondition.setParameter("propertyName", "timeStamp");
-            numberOfDaysCondition.setParameter("comparisonOperator", 
"greaterThan");
-            numberOfDaysCondition.setParameter("propertyValueDateExpr", "now-" 
+ numberOfDays + "d");
-            conditions.add(numberOfDaysCondition);
-        }
-        if (fromDate != null)  {
-            Condition startDateCondition = new Condition();
-            
startDateCondition.setConditionType(definitionsService.getConditionType("eventPropertyCondition"));
-            startDateCondition.setParameter("propertyName", "timeStamp");
-            startDateCondition.setParameter("comparisonOperator", 
"greaterThanOrEqualTo");
-            startDateCondition.setParameter("propertyValueDate", fromDate);
-            conditions.add(startDateCondition);
-        }
-        if (toDate != null)  {
-            Condition endDateCondition = new Condition();
-            
endDateCondition.setConditionType(definitionsService.getConditionType("eventPropertyCondition"));
-            endDateCondition.setParameter("propertyName", "timeStamp");
-            endDateCondition.setParameter("comparisonOperator", 
"lessThanOrEqualTo");
-            endDateCondition.setParameter("propertyValueDate", toDate);
-            conditions.add(endDateCondition);
-        }
+            Condition eventCondition = (Condition) 
pastEventCondition.getParameter("eventCondition");
+            if (eventCondition != null) {
+                
definitionsService.getConditionValidationService().validate(eventCondition);
+            }
+            conditions.add(eventCondition);
+
+            Condition c = new 
Condition(definitionsService.getConditionType("eventPropertyCondition"));
+            c.setParameter("propertyName", "profileId");
+            c.setParameter("comparisonOperator", "equals");
+            c.setParameter("propertyValue", event.getProfileId());
+            conditions.add(c);
+
+            // may be current event is already persisted and indexed, in that 
case we filter it from the count to increment it manually at the end
+            Condition eventIdFilter = new 
Condition(definitionsService.getConditionType("eventPropertyCondition"));
+            eventIdFilter.setParameter("propertyName", "itemId");
+            eventIdFilter.setParameter("comparisonOperator", "notEquals");
+            eventIdFilter.setParameter("propertyValue", event.getItemId());
+            conditions.add(eventIdFilter);
+
+            Integer numberOfDays = (Integer) 
pastEventCondition.getParameter("numberOfDays");

Review Comment:
   `numberOfDays` is cast to `Integer`, but it can be provided as a numeric 
string (existing tests cover this). This cast can throw `ClassCastException` 
and break event occurrence counting. Use `PropertyHelper.getInteger(...)` 
coercion instead.



##########
services/src/main/java/org/apache/unomi/services/impl/definitions/DefinitionsServiceImpl.java:
##########
@@ -573,9 +580,25 @@ public boolean resolveConditionType(Condition 
rootCondition) {
         // Delegate to TypeResolutionService for resolution
         boolean resolved = 
typeResolutionService.resolveConditionType(rootCondition, "condition type " + 
rootCondition.getConditionTypeId());
         if (resolved) {
+            // Start validation operation in tracer
+            if (tracerService != null) {
+                RequestTracer tracer = tracerService.getCurrentTracer();
+                if (tracer != null && tracer.isEnabled()) {
+                    tracer.startOperation("condition-validation", "Validating 
condition: " + rootCondition.getConditionTypeId(), rootCondition);
+                }
+            }
+
             // Validate the condition after resolving its type (validation 
service will auto-resolve if needed)
             List<ValidationError> validationErrors = 
conditionValidationService.validate(rootCondition);
 
+            // Add validation info to tracer
+            if (tracerService != null) {
+                RequestTracer tracer = tracerService.getCurrentTracer();
+                if (tracer != null && tracer.isEnabled()) {
+                    tracer.addValidationInfo(validationErrors, 
"condition-validation");
+                    tracer.endOperation(!validationErrors.isEmpty(), 
String.format("Validation completed with %d errors", validationErrors.size()));
+                }

Review Comment:
   The tracer operation result passed to `endOperation(...)` is inverted: 
`!validationErrors.isEmpty()` reports success when there are errors, making 
explain traces misleading.



##########
services/src/main/java/org/apache/unomi/services/impl/goals/GoalsServiceImpl.java:
##########
@@ -188,8 +198,119 @@ public void setGoal(Goal goal) {
             LOGGER.warn("Trying to save null goal, aborting...");
             return;
         }
-        ParserHelper.resolveConditionType(definitionsService, 
goal.getStartEvent(), "goal "+goal.getItemId()+" start event");
-        ParserHelper.resolveConditionType(definitionsService, 
goal.getTargetEvent(), "goal "+goal.getItemId()+" start event");
+        boolean isValid = true;
+        if (goal.getStartEvent() != null) {
+            // Resolve condition type (skips parameter resolution - happens 
on-demand in query builders/evaluators)
+            TypeResolutionService typeResolutionService = 
getTypeResolutionService();
+            boolean startValid = typeResolutionService != null && 
typeResolutionService.resolveCondition("goals", goal, goal.getStartEvent(), 
"goal "+goal.getItemId()+" start event");
+            isValid = isValid && startValid;
+            // Start validation operation in tracer for start event
+            if (tracerService != null) {
+                RequestTracer tracer = tracerService.getCurrentTracer();
+                if (tracer != null && tracer.isEnabled()) {
+                    tracer.startOperation("goal-start-event-validation", 
"Validating goal start event: " + goal.getItemId(), goal.getStartEvent());
+                }
+            }
+
+            // Validate condition (skips parameters with references/scripts)
+            // Validation service will auto-resolve types if needed
+            List<ValidationError> validationErrors = 
definitionsService.getConditionValidationService().validate(goal.getStartEvent());
+
+            // Add validation info to tracer
+            if (tracerService != null) {
+                RequestTracer tracer = tracerService.getCurrentTracer();
+                if (tracer != null && tracer.isEnabled()) {
+                    tracer.addValidationInfo(validationErrors, 
"goal-start-event-validation");
+                    tracer.endOperation(!validationErrors.isEmpty(), 
String.format("Goal start event validation completed with %d errors", 
validationErrors.size()));
+                }
+            }
+
+            // Separate errors and warnings
+            List<ValidationError> errors = validationErrors.stream()
+                .filter(error -> error.getType() != 
ValidationErrorType.MISSING_RECOMMENDED_PARAMETER)
+                .collect(Collectors.toList());
+
+            List<ValidationError> warnings = validationErrors.stream()
+                .filter(error -> error.getType() == 
ValidationErrorType.MISSING_RECOMMENDED_PARAMETER)
+                .collect(Collectors.toList());
+
+            // Log warnings but don't block the operation
+            if (!warnings.isEmpty()) {
+                StringBuilder warningMessage = new StringBuilder("Goal start 
event has warnings:");
+                for (ValidationError warning : warnings) {
+                    warningMessage.append("\n- ").append(warning.getMessage());
+                }
+                LOGGER.warn(warningMessage.toString());
+            }
+
+            // Only throw exception for actual errors
+            if (!errors.isEmpty()) {
+                StringBuilder errorMessage = new StringBuilder("Invalid goal 
start event:");
+                for (ValidationError error : errors) {
+                    errorMessage.append("\n- ").append(error.getMessage());
+                }
+                if (typeResolutionService != null) {
+                    typeResolutionService.markInvalid("goals", 
goal.getItemId(), "Start event validation errors: " + errorMessage.toString());
+                }
+                throw new IllegalArgumentException(errorMessage.toString());
+            }
+        }
+        if (goal.getTargetEvent() != null) {
+            // Resolve condition type (skips parameter resolution - happens 
on-demand in query builders/evaluators)
+            TypeResolutionService typeResolutionService = 
getTypeResolutionService();
+            boolean targetValid = typeResolutionService != null && 
typeResolutionService.resolveCondition("goals", goal, goal.getTargetEvent(), 
"goal "+goal.getItemId()+" target event");
+            isValid = isValid && targetValid;
+            // Start validation operation in tracer for target event
+            if (tracerService != null) {
+                RequestTracer tracer = tracerService.getCurrentTracer();
+                if (tracer != null && tracer.isEnabled()) {
+                    tracer.startOperation("goal-target-event-validation", 
"Validating goal target event: " + goal.getItemId(), goal.getTargetEvent());
+                }
+            }
+
+            // Validate condition (skips parameters with references/scripts)
+            // Validation service will auto-resolve types if needed
+            List<ValidationError> targetValidationErrors = 
definitionsService.getConditionValidationService().validate(goal.getTargetEvent());
+
+            // Add validation info to tracer
+            if (tracerService != null) {
+                RequestTracer tracer = tracerService.getCurrentTracer();
+                if (tracer != null && tracer.isEnabled()) {
+                    tracer.addValidationInfo(targetValidationErrors, 
"goal-target-event-validation");
+                    tracer.endOperation(!targetValidationErrors.isEmpty(), 
String.format("Goal target event validation completed with %d errors", 
targetValidationErrors.size()));
+                }

Review Comment:
   The tracer operation result passed to `endOperation(...)` is inverted: 
`!targetValidationErrors.isEmpty()` reports success when there are errors. This 
makes goal explain traces misleading.



##########
plugins/baseplugin/src/main/java/org/apache/unomi/plugins/baseplugin/actions/CopyPropertiesAction.java:
##########
@@ -37,49 +39,98 @@ public class CopyPropertiesAction implements ActionExecutor 
{
 
     private static final Logger LOGGER = 
LoggerFactory.getLogger(CopyPropertiesAction.class);
     private ProfileService profileService;
+    private TracerService tracerService;
 
     public void setProfileService(ProfileService profileService) {
         this.profileService = profileService;
     }
 
+    public void setTracerService(TracerService tracerService) {
+        this.tracerService = tracerService;
+    }
+
     @SuppressWarnings({ "unchecked", "rawtypes" })
     public int execute(Action action, Event event) {
-        boolean atLeastOnechanged = false;
-        List<String> mandatoryPropTypeSystemTags = (List<String>) 
action.getParameterValues().get("mandatoryPropTypeSystemTag");
-        String singleValueStrategy = (String) 
action.getParameterValues().get("singleValueStrategy");
-        for (Map.Entry<String, Object> entry : getEventPropsToCopy(action, 
event).entrySet()) {
-            String mappedProperty = resolvePropertyName(entry.getKey());
-
-            // propType Check
-            PropertyType propertyType = 
profileService.getPropertyType(mappedProperty);
-            Object previousValue = 
event.getProfile().getProperty(mappedProperty);
-            if (mandatoryPropTypeSystemTags != null && 
mandatoryPropTypeSystemTags.size() > 0) {
-                if (propertyType == null || propertyType.getMetadata() == null 
|| propertyType.getMetadata().getSystemTags() == null
-                        || 
!propertyType.getMetadata().getSystemTags().containsAll(mandatoryPropTypeSystemTags))
 {
-                    continue;
-                }
-            }
-            String propertyName = "properties." + mappedProperty;
-            boolean changed = false;
-            if (previousValue == null && propertyType == null) {
-                changed = PropertyHelper.setProperty(event.getProfile(), 
propertyName, entry.getValue(), "alwaysSet");
-            } else {
-                boolean propertyTypeIsMultiValued =
-                        propertyType != null && propertyType.isMultivalued() 
!= null && propertyType.isMultivalued();
-                boolean multipleIsExpected = previousValue != null ? 
previousValue instanceof List : propertyTypeIsMultiValued;
-
-                if (multipleIsExpected) {
-                    changed = PropertyHelper.setProperty(event.getProfile(), 
propertyName, entry.getValue(), "addValues");
-                } else if (entry.getValue() instanceof List) {
-                    LOGGER.error("Impossible to copy the property of type List 
to the profile, either a single value already exist on the profile or the 
property type is declared as a single value property. Enable debug log level 
for more information");
-                    LOGGER.debug("cannot copy property {}, because it's a 
List", mappedProperty);
-                } else {
-                    changed = PropertyHelper.setProperty(event.getProfile(), 
propertyName, entry.getValue(), singleValueStrategy);
+        RequestTracer tracer = tracerService.getCurrentTracer();
+        if (!tracer.isEnabled()) {
+            tracer.setEnabled(true);
+        }

Review Comment:
   This action forcibly enables tracing (`tracer.setEnabled(true)`) even when 
tracing was not requested. Since tracers are ThreadLocal, this can accidentally 
keep tracing enabled across later requests on the same thread. Tracing should 
be opt-in (only execute when `TracerService.isTracingEnabled()` is true) and 
should not be enabled from inside an action.



##########
plugins/baseplugin/src/main/java/org/apache/unomi/plugins/baseplugin/actions/EvaluateVisitPropertiesAction.java:
##########
@@ -31,47 +33,97 @@
 import java.text.SimpleDateFormat;
 import java.util.Date;
 import java.util.TimeZone;
+import java.util.HashMap;
 
 /**
  * This action is used to calculate the firstVisit, lastVisit and 
previousVisit date properties on the profile
  * Depending on the event timestamp it will adjust one or multiples of this 
properties accordingly to the logical chronology.
  */
 public class EvaluateVisitPropertiesAction implements ActionExecutor {
     private static final Logger LOGGER = 
LoggerFactory.getLogger(EvaluateVisitPropertiesAction.class.getName());
+    private TracerService tracerService;
+
+    public void setTracerService(TracerService tracerService) {
+        this.tracerService = tracerService;
+    }
 
     public int execute(Action action, Event event) {
-        SimpleDateFormat dateFormat = new 
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
-        dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+        RequestTracer tracer = tracerService.getCurrentTracer();
+        if (!tracer.isEnabled()) {
+            tracer.setEnabled(true);
+        }
 

Review Comment:
   This action forcibly enables tracing (`tracer.setEnabled(true)`) even when 
tracing was not requested. Since tracers are ThreadLocal, this can 
unintentionally keep tracing enabled for later requests on the same thread. 
Tracing should be opt-in (only run when `tracerService.isTracingEnabled()` is 
true) and never enabled from inside an action.



##########
plugins/baseplugin/src/main/java/org/apache/unomi/plugins/baseplugin/actions/EventToProfilePropertyAction.java:
##########
@@ -21,20 +21,63 @@
 import org.apache.unomi.api.actions.Action;
 import org.apache.unomi.api.actions.ActionExecutor;
 import org.apache.unomi.api.services.EventService;
+import org.apache.unomi.tracing.api.TracerService;
+import org.apache.unomi.tracing.api.RequestTracer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.HashMap;
 
 /**
  * A action to copy an event property to a profile property
  */
 public class EventToProfilePropertyAction implements ActionExecutor {
+    private static final Logger LOGGER = 
LoggerFactory.getLogger(EventToProfilePropertyAction.class);
+    private TracerService tracerService;
+
+    public void setTracerService(TracerService tracerService) {
+        this.tracerService = tracerService;
+    }
 
     public int execute(Action action, Event event) {
+        RequestTracer tracer = tracerService.getCurrentTracer();
+        if (!tracer.isEnabled()) {
+            tracer.setEnabled(true);
+        }

Review Comment:
   This action forcibly enables tracing (`tracer.setEnabled(true)`) even when 
tracing was not requested. Because the tracer is stored in a ThreadLocal, this 
can accidentally keep tracing enabled across subsequent requests handled by the 
same thread, increasing overhead and potentially leaking trace data into 
unrelated admin explain responses. Tracing should be opt-in (only when 
`TracerService.isTracingEnabled()` is true) and never enabled from inside an 
action.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


Reply via email to