Skip to content

feat: auto-disable webhooks after persistent failures#3587

Draft
JanCizmar wants to merge 4 commits intomainfrom
jancizmar/webhook-auto-disable
Draft

feat: auto-disable webhooks after persistent failures#3587
JanCizmar wants to merge 4 commits intomainfrom
jancizmar/webhook-auto-disable

Conversation

@JanCizmar
Copy link
Copy Markdown
Member

@JanCizmar JanCizmar commented Apr 2, 2026

Summary

  • Automatically disable webhooks that fail continuously for 3 days
  • Send warning email to organization owners after 6 hours of continuous failure
  • Send notification email when webhook is disabled
  • Add enabled/disabled toggle in the webhook UI
  • Fix bug where firstFailed was overwritten on every failure instead of tracking the initial failure timestamp

Changes

Backend

  • Add enabled and autoDisableNotified fields to WebhookConfig entity with DB migrations
  • Fix firstFailed tracking in WebhookProcessor — only set on first failure, clear on success
  • Skip disabled webhooks during processing
  • Add WebhookAutoDisableScheduler using SchedulingManager + LockingProvider pattern (same as KeyTrashPurgeScheduler)
  • Add WebhookProperties for configurable thresholds (warning after 6h, disable after 3d)
  • Disabled in test config to avoid interfering with Spring context caching

Email

  • New webhook-disabled.tsx React Email template
  • New webhook-failing-warning.tsx React Email template
  • i18n keys for both templates

Frontend

  • Add Switch toggle to WebhookItem with disabled state (dimmed row, tooltip)
  • Update API schema with enabled field

Tests

  • WebhookAutoDisableSchedulerTest (8 tests): disable after 3 days, warning after 6 hours, no duplicate warnings, email sent to owners
  • WebhookAutomationTest (2 new tests): firstFailed not overwritten on subsequent failures, disabled webhooks skipped
  • WebhookConfigControllerTest (4 new tests): enabled field CRUD, re-enable clears firstFailed
  • E2E (1 new test): toggle webhook enabled/disabled via UI

Test plan

  • All 23 backend tests pass
  • TypeScript compiles
  • ESLint passes
  • Kotlin formatting passes
  • Email templates export successfully
  • E2E webhook toggle test passes in CI

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Add webhook enable/disable (API + UI toggle) with visual dimming for disabled items
    • Introduce configurable auto-disable scheduler that warns then auto-disables persistently failing webhooks
    • Add localized warning and disabled email templates
  • Tests

    • Added tests covering toggle behavior, skipping delivery for disabled webhooks, warning notifications, and auto-disable scenarios
  • Documentation

    • Added configuration properties for webhook auto-disable behavior and English email strings

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 2, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 67757b73-cba1-4f74-9cb9-1fbd996b57a5

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds an enabled flag to webhook configs, persists it, prevents processing of disabled webhooks, introduces auto-disable scheduler with warning/disable emails, updates API + frontend toggle, database migration, configuration properties, and tests for the new behaviors.

Changes

Cohort / File(s) Summary
Models & DB
backend/api/src/main/kotlin/io/tolgee/hateoas/ee/webhooks/WebhookConfigModel.kt, backend/data/src/main/kotlin/io/tolgee/model/webhook/WebhookConfig.kt, backend/data/src/main/resources/db/changelog/schema.xml, backend/data/src/main/kotlin/io/tolgee/dtos/request/WebhookConfigRequest.kt
Added enabled: Boolean to model/DTO/entity and autoDisableNotified: Boolean to entity; added Liquibase migration columns (enabled, auto_disable_notified).
Processor & Repository
backend/data/src/main/kotlin/io/tolgee/component/automations/processors/WebhookProcessor.kt, backend/data/src/main/kotlin/io/tolgee/repository/WebhookConfigRepository.kt
Processor early-returns for disabled webhooks; preserve firstFailed on repeated failures and clear on recovery; added repository queries to find enabled failing webhooks and those not yet notified.
Service & Scheduler
ee/backend/app/src/main/kotlin/io/tolgee/ee/service/WebhookConfigService.kt, ee/backend/app/src/main/kotlin/io/tolgee/ee/component/WebhookAutoDisableScheduler.kt
Service updates enabled state, reset behavior on re-enable, and adds methods to mark warnings/disable webhooks; new scheduler acquires a distributed lock, finds webhooks to warn/disable, updates state, and sends emails.
Configuration
backend/data/src/main/kotlin/io/tolgee/configuration/tolgee/WebhookProperties.kt, backend/data/src/main/kotlin/io/tolgee/configuration/tolgee/TolgeeProperties.kt, backend/app/src/test/resources/application.yaml
Introduced tolgee.webhook properties (auto-disable toggles, timings) and added test config override.
API & Assembler
ee/backend/app/src/main/kotlin/io/tolgee/ee/api/v2/hateoas/assemblers/WebhookConfigModelAssembler.kt, webapp/src/service/apiSchema.generated.ts
Assembler maps new enabled field into API model; OpenAPI-generated schema updated for WebhookConfigModel and WebhookConfigRequest.
Frontend
webapp/src/ee/developer/webhook/WebhookItem.tsx, e2e/cypress/support/dataCyType.d.ts, e2e/cypress/e2e/projects/webhooks.cy.ts
Added toggle UI and mutation for enabling/disabling webhooks, visual dimming for disabled items, added data-cy id and e2e tests validating toggle behavior.
Emails & Localization
email/emails/webhook-failing-warning.tsx, email/emails/webhook-disabled.tsx, email/i18n/messages_en.properties
Added two email templates (warning and disabled) and English localization keys used by scheduler notifications.
Tests
ee/backend/tests/src/test/kotlin/io/tolgee/ee/WebhookAutomationTest.kt, ee/backend/tests/src/test/kotlin/io/tolgee/ee/component/WebhookAutoDisableSchedulerTest.kt, ee/backend/tests/src/test/kotlin/io/tolgee/ee/api/v2/controllers/WebhookConfigControllerTest.kt
Added unit/integration tests for firstFailed preservation, skipping disabled webhooks, controller enabled behavior, scheduler auto-disable and warning flows, and email side effects.

Sequence Diagram

sequenceDiagram
    participant Scheduler as WebhookAutoDisableScheduler
    participant Locking as LockingProvider
    participant Service as WebhookConfigService
    participant Repo as WebhookConfigRepository
    participant Email as EmailService
    participant DB as Database

    Scheduler->>Locking: withLockingIfFree(lockName, leaseTime)
    alt lock acquired
        Scheduler->>Repo: findEnabledFailingNotNotified(warningCutoff)
        Repo-->>Scheduler: webhooksToWarn
        loop each webhook
            Scheduler->>Service: markWarningNotified(webhook)
            Service->>DB: set autoDisableNotified = true
            Scheduler->>Email: send webhook-failing-warning to org owners
        end
        Scheduler->>Repo: findEnabledFailingSince(disableCutoff)
        Repo-->>Scheduler: webhooksToDisable
        loop each webhook
            Scheduler->>Service: disableWebhook(webhook)
            Service->>DB: set enabled = false
            Scheduler->>Email: send webhook-disabled to org owners
        end
    else lock not acquired
        Scheduler->>Scheduler: skip run
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • bdshadow
  • dkrizan

Poem

🐰 I hopped through code both day and night,
I nudged a toggle, set things right.
When hooks are tired they sleep with grace,
A warning first, then gentle place.
Hooray — safer webhooks in our space! 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: auto-disable webhooks after persistent failures' accurately and concisely summarizes the main feature addition - automatic disabling of webhooks after repeated failures over time.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch jancizmar/webhook-auto-disable

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🧹 Nitpick comments (3)
backend/data/src/main/kotlin/io/tolgee/configuration/tolgee/TolgeeProperties.kt (1)

123-123: Optional: document the new webhook config block in generated property docs.

Line 123 works as-is, but adding @DocProperty here would keep config docs as self-explanatory as the surrounding properties.

📝 Suggested refinement
+  `@DocProperty`(description = "Webhook automation settings (e.g. auto-disable controls).")
   var webhook: WebhookProperties = WebhookProperties(),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/data/src/main/kotlin/io/tolgee/configuration/tolgee/TolgeeProperties.kt`
at line 123, Add the `@DocProperty` annotation to the TolgeeProperties.webhook
property so the generated configuration docs include the new webhook config
block; update the declaration for the webhook property (type WebhookProperties
in class TolgeeProperties) to include `@DocProperty` with a brief description of
the webhook config and any relevant default behavior so it appears alongside the
other documented properties.
e2e/cypress/e2e/projects/webhooks.cy.ts (1)

116-156: Reload after each toggle to verify persistence.

This currently proves only that the local row state changed. A reload after disabling/enabling would catch cases where the UI updates optimistically but the PUT never persists enabled, and it would let the test assert the re-fetched switch state instead of the immediate DOM styling.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/cypress/e2e/projects/webhooks.cy.ts` around lines 116 - 156, After
toggling the webhook in the test (using createWebhook(),
item.findDcy('webhook-item-toggle').find('input').click() and
waitForGlobalLoading()), reload the page and re-query the item via gcyAdvanced({
value: 'webhooks-list-item', url: testUrl }) before asserting persistence; do
this both after disabling and after re-enabling so you assert the persisted
switch state (should('not.be.checked') / should('be.checked')) and the persisted
row opacity ('have.css','opacity','0.6' and '1') against the re-fetched DOM
rather than the immediate optimistic DOM updates.
backend/data/src/main/kotlin/io/tolgee/component/automations/processors/WebhookProcessor.kt (1)

29-32: Move the disabled guard before building the activity payload.

Disabled webhooks still pay for findProjectActivity() and toModel() because the enabled check happens afterward. Read action.webhookConfig and return on !enabled before Lines 29-30 so skipped hooks avoid the extra lookup and serialization.

♻️ Suggested reorder
   ) {
     activityRevisionId ?: return
+    val config = action.webhookConfig ?: return
+    if (!config.enabled) return
     val view = activityService.findProjectActivity(activityRevisionId) ?: return
     val activityModel = activityModelAssembler.toModel(view)
-    val config = action.webhookConfig ?: return
-    if (!config.enabled) return
 
     val data =
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/data/src/main/kotlin/io/tolgee/component/automations/processors/WebhookProcessor.kt`
around lines 29 - 32, In WebhookProcessor, avoid calling
activityService.findProjectActivity(...) and activityModelAssembler.toModel(...)
for disabled webhooks by reading action.webhookConfig and returning when
config.enabled is false before you build the activity payload; move the guard
that checks action.webhookConfig and config.enabled to execute prior to the
calls to findProjectActivity(...) and toModel(...) so disabled hooks skip the
lookup and serialization.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@backend/data/src/main/kotlin/io/tolgee/configuration/tolgee/WebhookProperties.kt`:
- Around line 6-33: Annotate the WebhookProperties class with Spring validation
(`@Validated`) and add bean-validation annotations to each numeric property (use
`@Positive` or `@Min`(1) for autoDisableCheckPeriodMs, autoDisableLockLeaseTimeMs
and `@Positive/`@Min(1) for autoDisableWarningAfterHours and autoDisableAfterDays)
so non-positive values fail startup; also add a `@PostConstruct` validation method
in WebhookProperties that checks autoDisableWarningAfterHours <
autoDisableAfterDays and throws an IllegalStateException with a clear message if
the ordering is invalid so misordered thresholds are rejected at startup.

In
`@ee/backend/app/src/main/kotlin/io/tolgee/ee/component/WebhookAutoDisableScheduler.kt`:
- Around line 63-68: In WebhookAutoDisableScheduler, don't persist the
autoDisableNotified/markWarningNotified flag before sending emails; instead call
webhookConfigService.markWarningNotified(webhook) only after sendEmails(webhook,
"webhook-failing-warning") succeeds and do that persistence outside the
executeInNewTransaction block to avoid including network I/O in the DB
transaction; also change sendEmails handling so failures surface (or return
per-recipient status) and only then update autoDisableNotified, and apply the
same change for the second occurrence (the block around lines 112-127) so
notifications are only marked when emails were actually dispatched.

In
`@ee/backend/tests/src/test/kotlin/io/tolgee/ee/component/WebhookAutoDisableSchedulerTest.kt`:
- Around line 142-155: The test does not verify that no second warning email was
sent; after calling webhookAutoDisableScheduler.checkAndDisable() in the test
does not send another email. Update the `does not send warning twice` test to
also assert the email/mock notification sender observed zero new sends for that
invocation (e.g., check the send method call count on your mail mock or
notification service mock) immediately after calling
`webhookAutoDisableScheduler.checkAndDisable()` and before reloading the config;
reference the test function name and the
`webhookAutoDisableScheduler.checkAndDisable()` call to locate where to add the
assertion.

In `@email/emails/webhook-disabled.tsx`:
- Line 41: The email copy in webhook-disabled.tsx currently hardcodes "more than
3 days" in the defaultValue string; replace that static text with a dynamic
value pulled from the app config or props (e.g., autoDisableDays or
autoDisableDuration) and interpolate it into the defaultValue alongside
{webhookUrl} and {projectName}; also ensure proper pluralization/formatting when
building the string (e.g., "after failing continuously for more than
{autoDisableDays} day(s)" or use a helper like formatDuration) so the message
reflects the actual configurable auto-disable threshold.

In `@email/emails/webhook-failing-warning.tsx`:
- Around line 11-14: The multiline JSX expression for the subject prop uses
t.raw across multiple lines which trips the template static-check; collapse the
call into a single-line expression by replacing the current multiline
subject={t.raw('webhook-failing-warning-subject', 'Webhook is failing')} block
with a single-line subject={t.raw('webhook-failing-warning-subject', 'Webhook is
failing')} so the t.raw invocation is flattened (locate the subject prop inside
the email component in webhook-failing-warning.tsx).

In `@email/i18n/messages_en.properties`:
- Around line 18-21: The email templates currently hardcode the auto-disable
timeout; update the keys webhook-disabled-body and webhook-failing-warning-body
to use a formatted placeholder (e.g., {autoDisableAfter} or {remainingTime})
instead of the literal "more than 3 days" and "{autoDisableAfterDays} days", and
ensure the placeholder name is consistent across templates so callers can inject
the configured duration (formatted as "3 days" or "48 hours" as appropriate);
keep webhook-disabled-action and webhook-failing-warning-subject unchanged and
update any code that sends these messages to supply the new placeholder value so
default copy remains consistent with configuration.

In `@webapp/src/ee/developer/webhook/WebhookItem.tsx`:
- Around line 101-110: The tooltip logic currently infers "Automatically
disabled…" using isAutoDisabled which only checks data.firstFailed; change it to
rely on an explicit backend flag (e.g. data.autoDisabled or data.disabledReason
=== 'auto') instead of data.firstFailed, and if that flag is absent use a
generic disabled tooltip; update the isAutoDisabled and toggleTooltip branches
(referencing isAutoDisabled, toggleTooltip, data.firstFailed, data.enabled) so
only a true explicit auto-disabled flag shows the "Automatically disabled…" text
and manual disables show a generic "Webhook disabled" message or the existing
enable/disable hints.
- Around line 120-126: The Switch control (the Switch component with props
checked={data.enabled}, onChange={handleToggle},
disabled={toggleWebhook.isLoading}, data-cy="webhook-item-toggle") is missing an
accessible name; add an aria-label or aria-labelledby that identifies the
webhook (for example including data.name or data.id) or describes the action
("Enable webhook <name>" / "Disable webhook <name>") so screen readers announce
the toggle; ensure the label updates or is dynamic based on data.enabled and use
aria-labelledby if there's an existing visible element ID that names the
webhook.

---

Nitpick comments:
In
`@backend/data/src/main/kotlin/io/tolgee/component/automations/processors/WebhookProcessor.kt`:
- Around line 29-32: In WebhookProcessor, avoid calling
activityService.findProjectActivity(...) and activityModelAssembler.toModel(...)
for disabled webhooks by reading action.webhookConfig and returning when
config.enabled is false before you build the activity payload; move the guard
that checks action.webhookConfig and config.enabled to execute prior to the
calls to findProjectActivity(...) and toModel(...) so disabled hooks skip the
lookup and serialization.

In
`@backend/data/src/main/kotlin/io/tolgee/configuration/tolgee/TolgeeProperties.kt`:
- Line 123: Add the `@DocProperty` annotation to the TolgeeProperties.webhook
property so the generated configuration docs include the new webhook config
block; update the declaration for the webhook property (type WebhookProperties
in class TolgeeProperties) to include `@DocProperty` with a brief description of
the webhook config and any relevant default behavior so it appears alongside the
other documented properties.

In `@e2e/cypress/e2e/projects/webhooks.cy.ts`:
- Around line 116-156: After toggling the webhook in the test (using
createWebhook(), item.findDcy('webhook-item-toggle').find('input').click() and
waitForGlobalLoading()), reload the page and re-query the item via gcyAdvanced({
value: 'webhooks-list-item', url: testUrl }) before asserting persistence; do
this both after disabling and after re-enabling so you assert the persisted
switch state (should('not.be.checked') / should('be.checked')) and the persisted
row opacity ('have.css','opacity','0.6' and '1') against the re-fetched DOM
rather than the immediate optimistic DOM updates.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2c4a6e33-043e-4a78-89a5-ea381300f90a

📥 Commits

Reviewing files that changed from the base of the PR and between 7774f82 and 69cb09d.

📒 Files selected for processing (21)
  • backend/api/src/main/kotlin/io/tolgee/hateoas/ee/webhooks/WebhookConfigModel.kt
  • backend/app/src/test/resources/application.yaml
  • backend/data/src/main/kotlin/io/tolgee/component/automations/processors/WebhookProcessor.kt
  • backend/data/src/main/kotlin/io/tolgee/configuration/tolgee/TolgeeProperties.kt
  • backend/data/src/main/kotlin/io/tolgee/configuration/tolgee/WebhookProperties.kt
  • backend/data/src/main/kotlin/io/tolgee/dtos/request/WebhookConfigRequest.kt
  • backend/data/src/main/kotlin/io/tolgee/model/webhook/WebhookConfig.kt
  • backend/data/src/main/kotlin/io/tolgee/repository/WebhookConfigRepository.kt
  • backend/data/src/main/resources/db/changelog/schema.xml
  • e2e/cypress/e2e/projects/webhooks.cy.ts
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/api/v2/hateoas/assemblers/WebhookConfigModelAssembler.kt
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/component/WebhookAutoDisableScheduler.kt
  • ee/backend/app/src/main/kotlin/io/tolgee/ee/service/WebhookConfigService.kt
  • ee/backend/tests/src/test/kotlin/io/tolgee/ee/WebhookAutomationTest.kt
  • ee/backend/tests/src/test/kotlin/io/tolgee/ee/api/v2/controllers/WebhookConfigControllerTest.kt
  • ee/backend/tests/src/test/kotlin/io/tolgee/ee/component/WebhookAutoDisableSchedulerTest.kt
  • email/emails/webhook-disabled.tsx
  • email/emails/webhook-failing-warning.tsx
  • email/i18n/messages_en.properties
  • webapp/src/ee/developer/webhook/WebhookItem.tsx
  • webapp/src/service/apiSchema.generated.ts

Comment on lines +6 to +33
@ConfigurationProperties(prefix = "tolgee.webhook")
@DocProperty(description = "Configuration for webhook behavior.", displayName = "Webhooks")
class WebhookProperties {
@DocProperty(
description = "Whether the automatic disabling of failing webhooks is enabled.",
)
var autoDisableEnabled: Boolean = true

@DocProperty(
description = "How often (in milliseconds) to check for webhooks to auto-disable.",
)
var autoDisableCheckPeriodMs: Long = 3_600_000

@DocProperty(
description = "Number of hours of continuous failure before a warning email is sent.",
)
var autoDisableWarningAfterHours: Int = 6

@DocProperty(
description = "Number of days of continuous failure before a webhook is automatically disabled.",
)
var autoDisableAfterDays: Int = 3

@DocProperty(
description = "Lease time (in milliseconds) for the distributed lock used by the auto-disable job.",
)
var autoDisableLockLeaseTimeMs: Long = 600_000
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fail fast on invalid webhook auto-disable settings.

These values are trusted as-is. Non-positive periods/lease times can break scheduling, and a misordered warning/disable threshold can make webhooks eligible for disable before any warning is ever valid. Please add bean validation so bad config is rejected at startup.

Suggested hardening
+import jakarta.validation.constraints.AssertTrue
+import jakarta.validation.constraints.Min
+import jakarta.validation.constraints.Positive
 import org.springframework.boot.context.properties.ConfigurationProperties
+import org.springframework.validation.annotation.Validated

 `@ConfigurationProperties`(prefix = "tolgee.webhook")
+@Validated
 `@DocProperty`(description = "Configuration for webhook behavior.", displayName = "Webhooks")
 class WebhookProperties {
@@
+  `@field`:Positive
   var autoDisableCheckPeriodMs: Long = 3_600_000
@@
+  `@field`:Min(1)
   var autoDisableWarningAfterHours: Int = 6
@@
+  `@field`:Min(1)
   var autoDisableAfterDays: Int = 3
@@
+  `@field`:Positive
   var autoDisableLockLeaseTimeMs: Long = 600_000
+
+  `@get`:AssertTrue(
+    message =
+      "tolgee.webhook.autoDisableWarningAfterHours must be lower than autoDisableAfterDays * 24",
+  )
+  val isThresholdOrderValid: Boolean
+    get() = autoDisableWarningAfterHours < autoDisableAfterDays * 24
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@backend/data/src/main/kotlin/io/tolgee/configuration/tolgee/WebhookProperties.kt`
around lines 6 - 33, Annotate the WebhookProperties class with Spring validation
(`@Validated`) and add bean-validation annotations to each numeric property (use
`@Positive` or `@Min`(1) for autoDisableCheckPeriodMs, autoDisableLockLeaseTimeMs
and `@Positive/`@Min(1) for autoDisableWarningAfterHours and autoDisableAfterDays)
so non-positive values fail startup; also add a `@PostConstruct` validation method
in WebhookProperties that checks autoDisableWarningAfterHours <
autoDisableAfterDays and throws an IllegalStateException with a clear message if
the ordering is invalid so misordered thresholds are rejected at startup.

Comment on lines +63 to +68
executeInNewTransaction(transactionManager) {
val webhooks = webhookConfigService.findWebhooksToWarn(warningCutoff)

webhooks.forEach { webhook ->
webhookConfigService.markWarningNotified(webhook)
sendEmails(webhook, "webhook-failing-warning")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Persist autoDisableNotified only after the warning email is actually sent.

Right now the flag is written before the email call, and sendEmails() swallows per-recipient failures. A transient mail issue will therefore suppress every later warning attempt even though no owner was notified. Move the state update behind a successful dispatch, ideally outside the transaction that performs the network I/O.

Also applies to: 112-127

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@ee/backend/app/src/main/kotlin/io/tolgee/ee/component/WebhookAutoDisableScheduler.kt`
around lines 63 - 68, In WebhookAutoDisableScheduler, don't persist the
autoDisableNotified/markWarningNotified flag before sending emails; instead call
webhookConfigService.markWarningNotified(webhook) only after sendEmails(webhook,
"webhook-failing-warning") succeeds and do that persistence outside the
executeInNewTransaction block to avoid including network I/O in the DB
transaction; also change sendEmails handling so failures surface (or return
per-recipient status) and only then update autoDisableNotified, and apply the
same change for the second occurrence (the block around lines 112-127) so
notifications are only marked when emails were actually dispatched.

Comment on lines +142 to +155
@Test
fun `does not send warning twice`() {
testData.webhookConfig.self.firstFailed = currentDateProvider.date.addMinutes(-420)
testData.webhookConfig.self.enabled = true
testData.webhookConfig.self.autoDisableNotified = true
testDataService.saveTestData(testData.root)

webhookAutoDisableScheduler.checkAndDisable()

// Should still be notified, but no new email
val config = webhookConfigService.get(testData.webhookConfig.self.id)
config.autoDisableNotified.assert.isTrue
config.enabled.assert.isTrue
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

This doesn't prove a second warning email wasn't sent.

The assertions only verify persisted flags, so the test still passes if the scheduler sends another warning. Please also assert that the email mock observed zero new sends in this path.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@ee/backend/tests/src/test/kotlin/io/tolgee/ee/component/WebhookAutoDisableSchedulerTest.kt`
around lines 142 - 155, The test does not verify that no second warning email
was sent; after calling webhookAutoDisableScheduler.checkAndDisable() in the
test does not send another email. Update the `does not send warning twice` test
to also assert the email/mock notification sender observed zero new sends for
that invocation (e.g., check the send method call count on your mail mock or
notification service mock) immediately after calling
`webhookAutoDisableScheduler.checkAndDisable()` and before reloading the config;
reference the test function name and the
`webhookAutoDisableScheduler.checkAndDisable()` call to locate where to add the
assertion.

Comment thread email/emails/webhook-disabled.tsx Outdated
<Text>
<LocalizedText
keyName="webhook-disabled-body"
defaultValue="The webhook {webhookUrl} in project {projectName} has been automatically disabled after failing continuously for more than 3 days."
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Avoid hardcoded disable duration in email copy.

Line 41 hardcodes “more than 3 days”, but auto-disable threshold is configurable. This can send incorrect information on customized instances.

✏️ Proposed text fix (config-agnostic)
-          defaultValue="The webhook {webhookUrl} in project {projectName} has been automatically disabled after failing continuously for more than 3 days."
+          defaultValue="The webhook {webhookUrl} in project {projectName} has been automatically disabled after failing continuously for an extended period."
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
defaultValue="The webhook {webhookUrl} in project {projectName} has been automatically disabled after failing continuously for more than 3 days."
defaultValue="The webhook {webhookUrl} in project {projectName} has been automatically disabled after failing continuously for an extended period."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@email/emails/webhook-disabled.tsx` at line 41, The email copy in
webhook-disabled.tsx currently hardcodes "more than 3 days" in the defaultValue
string; replace that static text with a dynamic value pulled from the app config
or props (e.g., autoDisableDays or autoDisableDuration) and interpolate it into
the defaultValue alongside {webhookUrl} and {projectName}; also ensure proper
pluralization/formatting when building the string (e.g., "after failing
continuously for more than {autoDisableDays} day(s)" or use a helper like
formatDuration) so the message reflects the actual configurable auto-disable
threshold.

Comment thread email/emails/webhook-failing-warning.tsx Outdated
Comment thread email/i18n/messages_en.properties Outdated
Comment on lines +18 to +21
webhook-disabled-body = The webhook {webhookUrl} in project {projectName} has been automatically disabled after failing continuously for more than 3 days.
webhook-disabled-action = You can re-enable it from the project's Developer settings in the Webhooks section.
webhook-failing-warning-subject = Webhook is failing
webhook-failing-warning-body = The webhook {webhookUrl} in project {projectName} has been failing continuously. If the issue is not resolved, the webhook will be automatically disabled in {autoDisableAfterDays} days.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Don't hardcode the auto-disable timeout in these strings.

The warning/disable thresholds are configurable, so "more than 3 days" and "{autoDisableAfterDays} days" become misleading as soon as an instance uses anything other than the default. The warning email also is not necessarily sent with 3 days remaining. Please switch these to a formatted duration or remaining-time placeholder and keep the template default copy aligned.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@email/i18n/messages_en.properties` around lines 18 - 21, The email templates
currently hardcode the auto-disable timeout; update the keys
webhook-disabled-body and webhook-failing-warning-body to use a formatted
placeholder (e.g., {autoDisableAfter} or {remainingTime}) instead of the literal
"more than 3 days" and "{autoDisableAfterDays} days", and ensure the placeholder
name is consistent across templates so callers can inject the configured
duration (formatted as "3 days" or "48 hours" as appropriate); keep
webhook-disabled-action and webhook-failing-warning-subject unchanged and update
any code that sends these messages to supply the new placeholder value so
default copy remains consistent with configuration.

Comment on lines +101 to +110
const isAutoDisabled = !data.enabled && data.firstFailed;

const toggleTooltip = isAutoDisabled
? t(
'webhook_auto_disabled_hint',
'Automatically disabled due to persistent failures'
)
: data.enabled
? t('webhook_toggle_disable_hint', 'Disable webhook')
: t('webhook_toggle_enable_hint', 'Enable webhook');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Don't infer the disable reason from firstFailed alone.

A webhook that a user disables manually while it is already failing will still satisfy this condition and get the “Automatically disabled…” tooltip. This needs an explicit backend reason flag, or the disabled branch should fall back to a generic tooltip instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@webapp/src/ee/developer/webhook/WebhookItem.tsx` around lines 101 - 110, The
tooltip logic currently infers "Automatically disabled…" using isAutoDisabled
which only checks data.firstFailed; change it to rely on an explicit backend
flag (e.g. data.autoDisabled or data.disabledReason === 'auto') instead of
data.firstFailed, and if that flag is absent use a generic disabled tooltip;
update the isAutoDisabled and toggleTooltip branches (referencing
isAutoDisabled, toggleTooltip, data.firstFailed, data.enabled) so only a true
explicit auto-disabled flag shows the "Automatically disabled…" text and manual
disables show a generic "Webhook disabled" message or the existing
enable/disable hints.

Comment on lines +120 to +126
<Switch
size="small"
checked={data.enabled}
onChange={handleToggle}
disabled={toggleWebhook.isLoading}
data-cy="webhook-item-toggle"
/>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add an accessible name to the toggle.

The tooltip text does not label the control for assistive tech, so this will be announced as an unlabeled switch/button. Please attach an aria-label/aria-labelledby that identifies the webhook or the enable/disable action.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@webapp/src/ee/developer/webhook/WebhookItem.tsx` around lines 120 - 126, The
Switch control (the Switch component with props checked={data.enabled},
onChange={handleToggle}, disabled={toggleWebhook.isLoading},
data-cy="webhook-item-toggle") is missing an accessible name; add an aria-label
or aria-labelledby that identifies the webhook (for example including data.name
or data.id) or describes the action ("Enable webhook <name>" / "Disable webhook
<name>") so screen readers announce the toggle; ensure the label updates or is
dynamic based on data.enabled and use aria-labelledby if there's an existing
visible element ID that names the webhook.

@JanCizmar JanCizmar marked this pull request as draft April 2, 2026 14:27
JanCizmar and others added 3 commits April 10, 2026 16:22
Add webhook enabled/disabled toggle with automatic disabling when a
webhook fails continuously for 3 days. Organization owners are notified
via email at two stages: a warning after 6 hours of failure, and a
notification when the webhook is disabled.

Changes:
- Add `enabled` and `autoDisableNotified` fields to WebhookConfig
- Fix firstFailed bug (was overwritten on every failure instead of only
  on first failure)
- Skip disabled webhooks during processing
- Add scheduled job using SchedulingManager + LockingProvider pattern
- Add React Email templates for warning and disable notifications
- Add UI toggle switch in webhook list
- Add backend integration tests and E2E tests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Validate webhook properties at startup (warning hours < disable days)
- Send warning email before marking autoDisableNotified (not after)
- Use configurable autoDisableAfterDays in disabled email template
- Assert no email sent in "does not send warning twice" test

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@JanCizmar JanCizmar force-pushed the jancizmar/webhook-auto-disable branch from eb4a9e7 to b9f065f Compare April 13, 2026 18:05
- Add confirmation dialog when disabling a webhook (no prompt on enable)
- Add defaultExplanation to WebhookProperties time-based fields
- Expose internal /webhook-auto-disable/run endpoint for E2E testing
- Refactor webhooks E2E tests to use compound objects
  (E2WebhooksView, E2WebhookItem, E2WebhookEditDialog)
- Add E2E test verifying the full auto-disable flow: set firstFailed
  via SQL, trigger scheduler, check webhook disabled + email sent
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant