elsa-core/src/modules/Elsa.Bpmn/Hosting/BpmnDiagnosticEventNames.cs
Sipke Schoorstra 952dfa05ff
feat(bpmn): project interpreter diagnostics onto the scope's execution log (#8058)
* feat(bpmn): project interpreter diagnostics onto the scope's execution log

Under Option A only bound work carries an activity id, so gateways, events and
flows had no per-element trace in the journal. BpmnScopeHost now projects each
new BpmnExecutionState.Diagnostics entry onto the scope's own execution log
before Prune() runs, keyed by element id, with a persisted high-water mark so
a resumed scope never re-emits one. The scope's own start and completion stay
out, since they are already journaled as the activity's own lifecycle. Event
names and the payload shape are documented as a public compatibility surface
for elsa-studio#1000 to mirror.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(bpmn): project element and flow diagnostics dropped by the FlowId exclusion

The diagnostics exclusion keyed on Kind == TokenEmitted && FlowId is null/empty
also dropped an error or cancel boundary's own token emission, since a
boundary fires without an inbound flow. Narrow the rule to skip only
diagnostics that name neither an element nor a flow -- the scope's own
terminal Completed summary -- so every diagnostic keyed on an element or a
flow, including a start event's and a boundary's, is projected.

Also make DiagnosticSequence resilient: TryParse instead of Parse, logging a
warning and skipping projection for an id that doesn't match diag:N rather
than faulting the evaluation. Add a reflection-based test that keeps
BpmnDiagnosticEventNames in lockstep with BpmnDiagnosticKind, and record the
diagnostics volume measurement in the wiki.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(bpmn): require exact diag:N ids and seed the diagnostics cursor from prior state

Reject any diagnostic id that is not the exact "diag:" prefix followed by a
non-negative integer, so a malformed id can no longer poison the durable
projection cursor and cause later, genuinely valid, lower-sequence
diagnostics to be skipped forever.

Also seed a missing cursor from the highest valid sequence in the scope's
prior persisted state instead of treating it as zero, so a scope persisted
before diagnostics projection existed does not replay every retained
historical diagnostic as new on its next evaluation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 19:01:06 -07:00

93 lines
4.7 KiB
C#

namespace Elsa.Bpmn.Hosting;
/// <summary>
/// The stable execution log <c>eventName</c> for each <c>Bpmn.Model.State.BpmnDiagnosticKind</c>, and the
/// <c>source</c> every one of them carries. <see cref="BpmnScopeHost"/> derives the event name directly from the
/// diagnostic kind's own enum member name; the constants here exist so Studio has one place to mirror them rather
/// than depending on the library's integer enum values.
/// </summary>
public static class BpmnDiagnosticEventNames
{
/// <summary>The <c>source</c> every execution log entry projected from a BPMN diagnostic carries.</summary>
public const string Source = "BPMN";
/// <summary>
/// A token arrived at an element via a sequence flow, or an element (a start event, or an error/cancel boundary
/// firing without an inbound flow) emitted a token of its own. Projected by <see cref="BpmnScopeHost"/> whenever
/// it names an element or a flow, which every one of these does.
/// </summary>
public const string TokenEmitted = "TokenEmitted";
/// <summary>An element started bound work: a single unit, or one instance of a multi-instance loop.</summary>
public const string Scheduled = "Scheduled";
/// <summary>A token arrived at a join and is waiting for its siblings.</summary>
public const string Waiting = "Waiting";
/// <summary>A join fired after its arrivals were satisfied.</summary>
public const string Joined = "Joined";
/// <summary>An end event consumed a token, or a multi-instance loop consumed a finished instance's token.</summary>
public const string Consumed = "Consumed";
/// <summary>A unit of work was cancelled.</summary>
public const string Canceled = "Canceled";
/// <summary>A terminate end event ended the process.</summary>
public const string Terminated = "Terminated";
/// <summary>An element's behavior failed.</summary>
public const string BehaviorFailure = "BehaviorFailure";
/// <summary>
/// The scope itself finished. Never projected by <see cref="BpmnScopeHost"/>: unlike every other kind, it names
/// neither an element nor a flow, and the scope's own activity lifecycle already journals its completion.
/// </summary>
public const string Completed = "Completed";
/// <summary>A unit of work faulted.</summary>
public const string Faulted = "Faulted";
/// <summary>A host completion carrying an attached compensation boundary registered a compensable.</summary>
public const string CompensationRegistered = "CompensationRegistered";
/// <summary>A compensate throw/end event triggered a compensation replay.</summary>
public const string CompensationTriggered = "CompensationTriggered";
/// <summary>A compensation handler ran to completion for one registered compensable.</summary>
public const string Compensated = "Compensated";
/// <summary>A cancel end event began (or completed) cancelling a transaction scope.</summary>
public const string TransactionCancelled = "TransactionCancelled";
/// <summary>An escalation throw/end event staged an enclosing-scope signal notification.</summary>
public const string EscalationRaised = "EscalationRaised";
/// <summary>An escalation notification matched an attached boundary and fired it.</summary>
public const string EscalationCaught = "EscalationCaught";
/// <summary>An escalation reached a scope that could not catch it; a no-op, never a fault.</summary>
public const string EscalationUnhandled = "EscalationUnhandled";
/// <summary>An interrupting escalation boundary matched a notification whose host had already terminalized; a no-op, never a fault.</summary>
public const string EscalationLate = "EscalationLate";
/// <summary>An event subprocess was activated by its start-event trigger.</summary>
public const string EventSubprocessActivated = "EventSubprocessActivated";
/// <summary>An event subprocess body ran to completion.</summary>
public const string EventSubprocessCompleted = "EventSubprocessCompleted";
/// <summary>A call activity's bound child failed and the engine routed the call-activity failure ladder instead of normal outbound flows.</summary>
public const string CallActivityFailureRouted = "CallActivityFailureRouted";
/// <summary>A message, signal or timer triggered scope listener was armed.</summary>
public const string ScopeListenerArmed = "ScopeListenerArmed";
/// <summary>A message, signal or timer triggered scope listener fired.</summary>
public const string ScopeListenerFired = "ScopeListenerFired";
/// <summary>A message, signal or timer triggered scope listener was retired.</summary>
public const string ScopeListenerRetired = "ScopeListenerRetired";
}