Skip to content

[Alerting V2] [UI] Rule form can still configure an inert recovery delay after server-side rejection #289390

Description

@baileycash-elastic

Problem

#288802 added server-side rejection of an inert recovery delay: create (createRuleDataSchema refine), update (validateMergedRuleAttributesINVALID_STATE_TRANSITION_CONFIG), and the manage_rule agent-builder tool now all reject state_transition.recovering_count > 0 / recovering_timeframe when recovery_strategy is none or unset.

The rule form was not updated to match, so the primary authoring surface can still build the payload the API now rejects. The result is a submit-time 400 surfaced as an error toast whose message points at state_transition.recovering_count — a path the form does not map to any control, so the user gets no indication that the Recovery delay control is the thing to change.

This is the remaining unmet acceptance criterion of #279941 (RecoveryDelayField hidden in the GUI whenever recovery delay would be inert), which that issue scoped as proceeding "regardless of the backend decision".

Reproduction

Reachable in a single create session. The flyout defaults are not what protect you — compose_discover_flyout.tsx defaults recoveryStrategy: 'no_breach' and stateTransitionRecoveryDelayMode: 'immediate', which is valid.

  1. Rules → Create rule (ES|QL compose flyout), advance to the Outcome step.
  2. Set Recovery to "No recovery" (composeDiscoverRecoveryType).
  3. Set Recovery delay to "Recoveries", leaving the default count of 2 (DEFAULT_RECOVERING_COUNT, recovery_delay_field.tsx).
  4. Save → composeFormToCreateRequest emits { recovery_strategy: 'none', state_transition: { recovering_count: 2 } }400.

Order does not matter. handleRecoveryTypeChange (compose_discover_flyout.tsx) clears the recovery query from both the sandbox and committed RHF state when leaving the query strategy, but leaves stateTransition.recoveringCount / recoveringTimeframe untouched — so arriving at "No recovery" from "Default recovery" with a delay already set is equally broken.

Hiding the control is not sufficient on its own

Hiding RecoveryDelayField when recoveryStrategy === 'none' makes step 2→3 above worse rather than better:

  • Pick "Default recovery", set Recovery delay to "Recoveries 2", then switch to "No recovery".
  • The control disappears, but form state still holds recoveringCount: 2 with stateTransitionRecoveryDelayMode: 'recoveries'.
  • mapStateTransition still emits recovering_count: 2, so the save still returns 400 — now with the offending control invisible.

So the fix must clear recoveringCount / recoveringTimeframe, in handleRecoveryTypeChange's non-query branch alongside the existing recovery-query clearing. Hiding or disabling the control is a good addition on top (a disabled control with help text explains why the delay is unavailable, which is closer to the intent of #279941), not a substitute.

Edit path

Stored rules with the inert combination open in the normal flyout rather than the YAML fallback: isNonRepresentableRule lists 'none' among the representable recovery strategies (is_non_representable.ts). mapRuleResponseToFormValues restores recoveringCount and deriveRecoveryDelayModeFromStateTransition derives 'recoveries', so saving an unrelated edit re-emits the value and returns 400.

The population of such rules is frozen as of #288802 (no write path can create a new one) and is believed to be negligible, so this is lower priority than the create path — but if the UI clears rather than hides, editing these rules also self-heals on next save.

Use the API's inert condition, not the one in #279941

Worth flagging for whoever picks this up, because implementing the condition as written in #279941 would leave a hole.

#279941 states the delay is inert exactly when recovery_strategy is null/'none' AND no_data_strategy !== 'recover', describing no_data_strategy: 'recover' as a carve-out where the delay is honoured. The engine does not behave that way, and committed unit tests assert the opposite:

  • count_timeframe_strategy.ts early-returns basicResult for a no_data event when no_data_strategy === 'recover', bypassing count/timeframe gating entirely.
  • count_timeframe_strategy.test.ts covers this with cases titled "transitions active → inactive immediately, ignoring recovery delay" and "transitions recovering → inactive immediately, ignoring recovery delay".
  • [Alerting V2] [ResponseOps] Recovery delay is configurable and persisted even when it has no effect on execution #288802 correspondingly rejects the combination (rejects an inert recovery delay even when no_data_strategy is "recover") and removed a now-invalid recovering_count: 1 from the director Scout spec.

The UI guard must therefore key off recovery_strategy == null || recovery_strategy === 'none', regardless of no_data_strategy, matching isRecoveryDelayAllowed in rule_data_schema.ts. Reusing that exported predicate directly is the safest option, since it keeps client and server conditions from drifting.

Separately, #279941's inert-condition paragraph should be corrected (or the director changed to honour the delay for no_data_strategy: 'recover', if that carve-out was the intended design). That reconciliation is out of scope here.

Acceptance criteria

  • Selecting "No recovery" clears stateTransition.recoveringCount / recoveringTimeframe so the form cannot emit an inert delay
  • RecoveryDelayField is hidden or disabled while recovery_strategy is none/unset, with help text explaining why
  • The guard condition matches isRecoveryDelayAllowed (ideally by reusing it) rather than [Alerting v2] Recovery delay is configurable and persisted even when it has no effect on execution #279941's stated condition
  • Editing a stored rule that already carries an inert delay no longer fails on save
  • Unit coverage in recovery_delay_field.test.tsx / compose_mappers for the strategy-switch clearing, asserting the mapped request omits the delay

References

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Feature:AlertingV2Team:ResponseOpsPlatform ResponseOps team (formerly the Cases and Alerting teams) t//Team:actionable-obsFormerly "obs-ux-management", responsible for SLO, o11y alerting, significant events, & synthetics.Team:rna-project

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions