Files
fusion/docs/signals-connectors.md
gsxdsm 98a5052b65 FN-6706: add signal connector incident ingestion
Connect external signal ingestion routes to Command Center incident analytics.\n\n- Add connector status reporting and incident upsert/resolution support for webhook, Sentry, Datadog, and PagerDuty sources.\n- Move signal aggregation through shared activity analytics and expose connector-aware empty states in Command Center.\n- Document connector setup, settings, and dashboard behavior with a published changeset.\n- Extend route, analytics, and Command Center UI tests for scoped signal data and configured-but-quiet connectors.\n\nFiles changed:\n .changeset/fn-6706-signal-connectors.md            |   7 +\n AGENTS.md                                          |   1 +\n docs/dashboard-guide.md                            |   4 +-\n docs/settings-reference.md                         |  16 +\n docs/signals-connectors.md                         | 140 ++++++++\n .../core/src/__tests__/signals-analytics.test.ts   |  37 ++-\n packages/core/src/activity-analytics.ts            | 185 ++++++++++-\n packages/core/src/index.ts                         |  11 +-\n packages/core/src/signals-analytics.ts             | 209 +-----------\n .../command-center/areas/SignalsArea.tsx           |  47 ++-\n .../areas/__tests__/areas.github-signals.test.tsx  | 120 +++++--\n .../command-center/areas/__tests__/areas.test.tsx  |   3 +-\n .../register-command-center-routes.test.ts         |  72 +++-\n .../src/__tests__/register-signal-routes.test.ts   | 369 ++++++++++++++++++++-\n .../src/routes/register-command-center-routes.ts   |  29 +-\n .../dashboard/src/routes/register-signal-routes.ts |  55 +++\n packages/dashboard/src/signal-source.ts            |   8 +\n packages/dashboard/src/signal-sources/datadog.ts   |   8 +\n packages/dashboard/src/signal-sources/pagerduty.ts |  14 +-\n packages/dashboard/src/signal-sources/sentry.ts    |  19 +-\n packages/dashboard/src/signal-sources/webhook.ts   |   8 +\n 21 files changed, 1094 insertions(+), 268 deletions(-)

Fusion-Task-Id: FN-6706

Fusion-Task-Lineage: 09003e9b-481b-4393-926b-44907d13bcf9
2026-06-26 00:28:31 -07:00

6.2 KiB

Signals Connectors

Fusion can receive signed external signals from Sentry, Datadog, PagerDuty, or a generic webhook at:

POST /api/signals/:provider

Supported providers are webhook, sentry, datadog, and pagerduty. Every connector requires an HMAC signing secret configured in the Fusion dashboard process environment. Verified signals still create triage tasks, and they also write to the project-scoped incidents table so Command Center → Signals can show source, severity, and open/resolved status breakdowns.

Runtime behavior

  • Open events create or absorb an incident occurrence by groupingKey and preserve the normalized source, severity, optional link, and capped meta fields.
  • Resolved events write/absorb the incident and then mark the matching groupingKey as resolved. Cold resolves are retained as resolved signal metrics instead of being dropped.
  • Duplicate deliveries with the same provider external id are accepted as deduped and do not create a second task or incident write.
  • Re-fired incidents with the same groupingKey are absorbed by the incidents store rather than double-counted as separate incidents.
  • The connectors status endpoint, GET /api/command-center/signals/connectors, returns only { provider, configured } booleans. It never returns secret values.

Security model

  • Secrets are environment variables; do not commit them to source control.
  • HMAC verification uses the raw request body and constant-time comparison.
  • Requests are capped at about 1 MB.
  • Replay protection rejects stale timestamps where the provider supplies one and rejects repeated delivery ids within the replay window.
  • Normalized title, body, groupingKey, link, and meta fields are capped by signal-source.ts before storage.
  • Signal link values are SSRF-untrusted. Fusion stores safe external URLs as data for the UI and never fetches connector links server-side.
  • meta is stored as JSON data only and must not be rendered as raw HTML.

Generic webhook

Set:

export FUSION_SIGNAL_WEBHOOK_SECRET="replace-with-a-long-random-secret"

Headers:

  • X-Fusion-Signature: sha256=-prefixed HMAC-SHA256 hex digest of the raw JSON body.
  • X-Fusion-Timestamp: epoch milliseconds, used for the replay window.

Payload contract:

{
  "id": "delivery-123",
  "title": "API error rate above threshold",
  "body": "5xx rate exceeded 10% for 5 minutes",
  "severity": "critical",
  "groupingKey": "api-error-rate",
  "link": "https://example.com/incidents/api-error-rate",
  "timestamp": 1790294400000,
  "status": "open",
  "meta": { "service": "api" }
}

Resolution mapping: status: "resolved", action: "resolved", or action: "resolve" resolves the grouped incident. Any other value opens/absorbs the incident.

Example signed request:

body='{"id":"delivery-123","title":"API error rate above threshold","severity":"critical","groupingKey":"api-error-rate","timestamp":'"$(date +%s000)"'}'
sig="sha256=$(printf '%s' "$body" | openssl dgst -sha256 -hmac "$FUSION_SIGNAL_WEBHOOK_SECRET" -hex | awk '{print $2}')"
curl -X POST "http://127.0.0.1:4040/api/signals/webhook" \
  -H "Content-Type: application/json" \
  -H "X-Fusion-Timestamp: $(date +%s000)" \
  -H "X-Fusion-Signature: $sig" \
  --data-binary "$body"

Sentry

Set:

export FUSION_SIGNAL_SENTRY_SECRET="sentry-integration-client-secret"

Configure the Sentry integration webhook target as /api/signals/sentry. Fusion verifies Sentry-Hook-Signature as the HMAC-SHA256 hex digest of the raw body. If Sentry sends Sentry-Hook-Timestamp, Fusion checks it against the replay window.

Normalization:

  • groupingKey: Sentry issue.id.
  • source: sentry.
  • severity: fatal/critical → critical, error → error, warning → warning, info/debug → info.
  • Resolution: payload action === "resolved" or issue.status === "resolved" resolves the grouped issue; other actions open/absorb it.

Datadog

Set:

export FUSION_SIGNAL_DATADOG_SECRET="datadog-webhook-shared-secret"

Datadog webhooks do not provide a built-in HMAC header, so configure a custom header:

X-Datadog-Signature: <HMAC-SHA256 hex digest of the raw body>

Optionally include X-Datadog-Timestamp as epoch milliseconds for replay-window validation. Configure the Datadog webhook target as /api/signals/datadog.

Normalization:

  • groupingKey: aggreg_key, alert_id, or id.
  • source: datadog.
  • severity: error → critical, warning/warn → warning, success/recovery/info → info, default → error.
  • Resolution: alert_type of recovery or success resolves the grouped monitor; other alert types open/absorb it.

PagerDuty

Set:

export FUSION_SIGNAL_PAGERDUTY_SECRET="pagerduty-webhook-subscription-secret"

Configure the PagerDuty webhook subscription target as /api/signals/pagerduty. Fusion verifies X-PagerDuty-Signature and accepts the v1=<hex> signature form.

Normalization:

  • groupingKey: PagerDuty incident data.id.
  • source: pagerduty.
  • severity: explicit data.severity when it is one of Fusion's normalized severities; otherwise high urgency maps to critical and other events map to warning.
  • Resolution: event.event_type === "incident.resolved" or data.status === "resolved" resolves the grouped incident; other incident events open/absorb it.

Command Center Signals

Command Center → Signals reads aggregated incidents through GET /api/command-center/signals. Once a connector secret is configured and signed events arrive, the area displays total, open, resolved, MTTR, by-source, by-severity, and by-status metrics from local incident rows.

The empty state is intentionally explicit:

  • no configured connector secret: prompt operators to connect Sentry, Datadog, PagerDuty, or the generic webhook;
  • at least one configured connector secret but no rows in the selected range: report that Fusion is configured and awaiting signals.

Separate monitor ingest path

FUSION_MONITOR_INGEST_SECRET protects the separate bearer-token route for /api/monitor/incidents. It is not used by /api/signals/:provider; signal connectors use the provider-specific FUSION_SIGNAL_*_SECRET variables above.