Skip to content

Debounce dashboard refresh dispatches - #176

Draft
trask wants to merge 2 commits into
open-telemetry:mainfrom
trask:webhook-debounce-refresh-dispatches
Draft

Debounce dashboard refresh dispatches#176
trask wants to merge 2 commits into
open-telemetry:mainfrom
trask:webhook-debounce-refresh-dispatches

Conversation

@trask

@trask trask commented Jul 28, 2026

Copy link
Copy Markdown
Member

Fixes #172

The webhook dispatches one workflow run per accepted event today, which is far more than the dashboard needs. GitHub Actions creates one check suite per workflow, so a single push to opentelemetry-collector-contrib produces 17 check_suite.completed webhooks and therefore 17 dispatches for one pull request. The worst burst measured was 53 dispatches in 77 seconds on a single pull request.

The webhook now records each accepted event in a dashboard-refresh-queue Netlify Blobs entry keyed by <repository>/<pr_number>, and a new scheduled function sweeps that queue every minute and dispatches once a pull request has been quiet for 45 seconds. Replaying 1,501 real webhooks from 15 recent collector-contrib pull requests through this policy yields 515 dispatches, a 66% reduction, and collapses the 53-event burst into 1.

Two alternatives were simulated and rejected: debouncing only check_suite events (52% reduction) and a leading-edge dispatch with a cooldown (54% reduction, plus more state). Debouncing everything is both simplest and most effective.

The cost is 45-105 seconds of added latency before a refresh starts. Routing is unaffected because ci_failing_since derives from each check's own completed_at rather than from refresh time. A dispatch failure leaves lastDispatchAt untouched, so the next sweep retries.

The GitHub App auth and dispatch code moves to netlify/lib/github-dispatch.mjs, which mints one installation token per sweep instead of one per webhook. The webhook now needs only GITHUB_WEBHOOK_SECRET and makes no GitHub API calls.

The deploy workflow gains an npm ci step because the functions now have a runtime dependency that Netlify has to bundle.

The webhook now records each accepted event in a Netlify Blobs queue, and a
scheduled function dispatches one workflow run per pull request once that pull
request has been quiet for 45 seconds.

Fixes open-telemetry#172
@opentelemetry-pr-dashboard

opentelemetry-pr-dashboard Bot commented Jul 28, 2026

Copy link
Copy Markdown

Pull request dashboard status

Waiting on the author · refreshed 2026-07-28 20:21 UTC

Move out of draft to request review.

Status above doesn't look right?
  • Just replied or pushed? Anything around or after the refresh time above may not be picked up yet — give it a few minutes.
  • Anything look wrong? Report it with what you expected; it helps us improve the dashboard.

Copilot AI left a comment

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.

Pull request overview

Adds a Netlify Blobs-backed debounce queue to reduce redundant dashboard refresh workflows.

Changes:

  • Queues webhook refresh events and flushes them after 45 seconds of inactivity.
  • Extracts GitHub authentication and dispatch logic.
  • Adds dependency installation, documentation, and queue tests.

Reviewed changes

Copilot reviewed 9 out of 10 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
.github/workflows/pull-request-dashboard-deploy-webhook.yml Installs function dependencies before deployment.
WEBHOOK_SETUP.md Documents queueing and scheduled dispatch behavior.
test_refresh_queue.mjs Tests queue keys, debounce, and expiry rules.
package.json Adds Netlify Blobs.
package-lock.json Locks new dependencies.
netlify/lib/refresh-queue.mjs Defines queue helpers and policies.
netlify/lib/github-dispatch.mjs Centralizes GitHub workflow dispatching.
netlify/functions/github-webhook.mjs Queues accepted webhook events.
netlify/functions/flush-dashboard-refreshes.mjs Sweeps and dispatches queued refreshes.
Files not reviewed (1)
  • .github/scripts/pull-request-dashboard/package-lock.json: Generated file
Comments suppressed due to low confidence (2)

.github/scripts/pull-request-dashboard/netlify/functions/flush-dashboard-refreshes.mjs:27

  • This deletion is based on a stale read. If a webhook updates an expired record after get returns but before this delete, the newly queued event is deleted and its dashboard refresh is lost. Delete conditionally using the ETag read with the record, and retry/re-evaluate on a conflict.
    const queued = await store.get(key, { type: "json" });
    if (isRefreshExpired(queued, now, EXPIRY_MS)) {
      await store.delete(key);

.github/scripts/pull-request-dashboard/netlify/functions/flush-dashboard-refreshes.mjs:48

  • Writing the stale queued snapshot after dispatch can drop a webhook that arrives during the dispatch. The webhook may first store a newer lastEventAt, then this write restores the older value and sets lastDispatchAt later than it, making isRefreshDue permanently consider the new event handled. Make this an ETag-conditional update and retry/re-evaluate conflicts so an event cannot be overwritten.
      await dispatch({
        repository: target.repository,
        pr_number: target.prNumber,
        trigger_event: queued.triggerEvent,
      });
      await store.setJSON(key, { ...queued, lastDispatchAt: Date.now() });

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +93 to 98
const queued = (await store.get(key, { type: "json" })) || {};
await store.setJSON(key, {
...queued,
lastEventAt: Date.now(),
triggerEvent: eventName,
});
Comment on lines +7 to +8
"@github/copilot": "1.0.75",
"@netlify/blobs": "^10.7.9"
export const config = { schedule: "* * * * *" };

export default async () => {
const store = getStore(REFRESH_QUEUE_STORE);
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.

Debounce dashboard refresh dispatches in the webhook function

2 participants