Skip to content

CP-14167 - Add webhook health monitoring for ATS integration#76

Open
bernardodsanderson wants to merge 2 commits into
mainfrom
CP-14167
Open

CP-14167 - Add webhook health monitoring for ATS integration#76
bernardodsanderson wants to merge 2 commits into
mainfrom
CP-14167

Conversation

@bernardodsanderson

Copy link
Copy Markdown
Collaborator

CP-14167

What

Adds periodic detection of ATS-linked accounts/partnerships missing their ATS-pointed WebhookUrl, and instruments outbound webhook delivery with New Relic metrics + Airbrake alerts on transport failures (previously swallowed silently).

Why

DocuSeal task completions silently stopped syncing to the ATS for ~4 months because nothing flagged the drift. These guardrails catch a missing WebhookUrl or a broken outbound delivery within hours instead of months, complementing the CP-14163 boot-time env-var guard.

How to test

  • CAREERPLUG_WEBHOOK_URL=... bundle exec rake careerplug:check_webhooks — flags accounts missing the ATS webhook
  • bundle exec rspec spec/lib/webhook_health_check_spec.rb spec/lib/send_webhook_request_spec.rb
  • Trigger a transport error (webhook URL pointing at a dead port) and confirm Airbrake.notify fires

What
Adds periodic detection of ATS-linked accounts/partnerships missing their ATS-pointed WebhookUrl, and instruments outbound webhook delivery with New Relic metrics + Airbrake alerts on transport failures (previously swallowed silently).

Why
DocuSeal task completions silently stopped syncing to the ATS for ~4 months because nothing flagged the drift. These guardrails catch a missing WebhookUrl or a broken outbound delivery within hours instead of months, complementing the CP-14163 boot-time env-var guard.

How to test
- `CAREERPLUG_WEBHOOK_URL=... bundle exec rake careerplug:check_webhooks` — flags accounts missing the ATS webhook
- `bundle exec rspec spec/lib/webhook_health_check_spec.rb spec/lib/send_webhook_request_spec.rb`
- Trigger a transport error (webhook URL pointing at a dead port) and confirm Airbrake.notify fires

@spaulsandhu spaulsandhu left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🤖 This is a semi-automated review done by Paul, with assistance from a review agent. Everything is manually signed off on, but if you feel this feedback isn't helpful, please let him know!

Clean observability slice for the DocuSeal->ATS webhook drift, and the specs are genuinely thorough. Nothing blocking from me, just a couple of questions: is the partnership scoping intentional, and what actually alerts us on a sustained non-2xx (vs a transport error)?

Nice, this is a clean little guardrail~ webhook drift going unnoticed for ~4 months is exactly the kind of thing worth instrumenting. Specs are thorough too, you covered the account/partnership/archived/blank-env branches and the 2xx / non-2xx / transport split. Also 👍🏽 on the .where.not(owner_column => nil) in the subquery (sidesteps the classic NOT IN + NULL footgun) and the defined?(::NewRelic::Agent) no-op guard.

—Paul-bot

.where.not(external_account_id: nil)
.where.not(id: ats_webhook_owner_ids(target_sha1, :account_id))

partnerships = Partnership.where.not(id: ats_webhook_owner_ids(target_sha1, :partnership_id))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Just confirming my read here~ on the account side we scope to .active + external_account_id present, but Partnership gets no equivalent filter. I'm assuming that's fine because every partnership is inherently ATS-linked (external_partnership_id is not null) and there's no archived state on them, so "all partnerships missing the hook" is correct? If so, all good, just want to make sure we're not flagging partnerships that were never meant to have the ATS webhook.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Correct read. Every Partnership is ATS-linked (external_partnership_id not null + uniqueness validation), there’s no archived/soft-delete on partnerships, and after_commit :create_careerplug_webhook always installs the ATS webhook on create. Accounts need the extra filters because they can be archived and not every account is external/ATS-linked. So “all partnerships missing the hook” is intentional.

Rails.logger.error("Webhook transport error (#{event_type}): #{e.class} #{e.message}")
Airbrake.notify("DocuSeal outbound webhook transport error (#{event_type}): #{e.class}")
nil
else

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[Conversational] More a question than a blocker~ a transport error pages Airbrake, but a non-2xx (say the ATS endpoint is 500ing for a sustained stretch) just records a failure metric and leans on the retry logic. If the ATS is down for a while and retries exhaust, does anything actually alert us, or is the NR dashboard the intended surface for that? Totally fine if the answer is "dashboard + retries is enough for now", I just want to make sure a prolonged outage isn't only visible if someone happens to be looking at the dashboard. 🤔

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Right — intentional split for this ticket:

• Transport errors were previously swallowed to nil with no signal → now log + Airbrake + NR failure metric.
• Non-2xx already participated in WebhookRetryLogic (retry on status >= 400 or nil, up to 10 attempts with exponential backoff). We added NR success/failure metrics so sustained non-2xx shows on the dashboard without page-spamming Airbrake on every 5xx.

After retries exhaust there is no dedicated “final failure” Airbrake today — NR is the surface for that path. A prolonged ATS 500 storm would also eventually show up on the ATS side via stuck TaskAssignment monitoring (CP-14167 ATS PR). Happy to follow up with an alert on final exhausted attempt if we want page-level signal for that path without waiting on the stuck-assignment threshold.

Comment thread bin/run_rake_task Outdated
exit 1
fi

if ! command -v aws &> /dev/null; then

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nitpick, not a blocker, and I'm no shell wizard: this is #!/bin/sh but command -v aws &> /dev/null uses &>, which is a bash-ism, under dash/busybox sh that parses as backgrounding the command plus a redirect, so the if ! guard wouldn't check what it looks like it checks. Same idea with the if [ $? -ne 0 ] blocks after the aws ... assignments, set -e has already bailed by then so they're effectively dead. Neither's dangerous since it fails closed either way, just flagging in case /bin/sh isn't bash in the container image.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Good nits — image is Alpine so /bin/sh isn’t bash. Switched &> to >/dev/null 2>&1 and replaced the dead $? checks after aws under set -e with if ! VAR=$(aws ...); then ...; fi. Same pattern exists in the start scripts; leaving those alone for this PR unless we want a small cleanup pass.

@bernardodsanderson

Copy link
Copy Markdown
Collaborator Author

Thanks Paul! Appreciate the thorough pass.
• Partnership scope is intentional (every partnership is ATS-linked; no archive filter).
• Non-2xx is NR + retries by design for this ticket; transport errors are the Airbrake path; final-exhausted-attempt alert is a reasonable follow-up.
• Shell nits on run_rake_task fixed (POSIX redirects + no dead $? checks).

What
Hardens the one-shot rake task runner so it works under Alpine’s /bin/sh (busybox ash), not just bash. AWS CLI detection and Secrets Manager fetches now fail cleanly with portable redirects and explicit error handling.

Why
The webhook health check runs as a scheduled ECS task via this script. Bash-isms (&>) and dead $? checks under set -e could mis-handle errors on Alpine, so the periodic integrity check needs a portable, fail-closed startup path.

How to test
• sh -n bin/run_rake_task (syntax check)
• Review that command -v aws uses >/dev/null 2>&1 and aws fetches use if ! VAR=$(aws ...); then
• On staging after deploy: EventBridge/ECS one-shot still loads secrets and runs careerplug:check_webhooks
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.

2 participants