Skip to content

feat(workflows): richer observability for push notifications - #70913

Merged
dmarchuk merged 2 commits into
masterfrom
claude/push-observability
Jul 15, 2026
Merged

feat(workflows): richer observability for push notifications#70913
dmarchuk merged 2 commits into
masterfrom
claude/push-observability

Conversation

@dmarchuk

@dmarchuk dmarchuk commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Problem

Push logging and metrics are much thinner than email, so when a customer's push notifications don't land they can't easily tell what happened. Three concrete gaps:

  • Opaque failure logs. On any failure the workflow log dumps the raw FCM/APNs JSON error at the user (FCM send error. Status: 400. Body: {...}). Fine for us, useless for a customer.
  • Mislabeled metrics. Push send metrics are emitted as metric_kind: 'other' even though a dedicated 'push' kind already exists (email correctly uses 'email').
  • No failure visibility in ops. The only Prometheus signal was a per-platform "sent" counter. Nothing told us why sends were failing, how many were skipped, or how slow they were.

Push has real limits vs email: FCM/APNs respond synchronously and there is no delivery-receipt webhook like SES, so there's no honest delivered/opened/clicked. This focuses on what we can actually observe at send time.

Changes

A failure-reason normalizer (push-notification-errors.ts) maps FCM error codes / APNs reasons / HTTP status into a small bounded set — unregistered, invalid_token, auth_error, rate_limited, invalid_payload, provider_error, network_error, unknown — each with:

  • a plain-language, action-guiding message (e.g. an auth_error tells the user which credential to check), and
  • a severity: config problems a user must fix are errors, transient or expected ones (throttle, dead token, 5xx) are warnings.

Logging now uses it. Instead of a raw payload dump, the workflow log shows a readable one-liner at the right level. The raw provider response is kept at debug level for support. Success reads "accepted by FCM/APNs" rather than "sent" — honest, since we only know the provider accepted it, not that the device displayed it.

Metrics. Business metrics now use the correct metric_kind: 'push'. Ops metrics gain reason/skip/latency dimensions:

metric labels meaning
push_notification_failed_total platform, reason terminal failures, by normalized reason
push_notification_skipped_total platform recipient had no registered device token
push_notification_send_duration_ms platform provider request latency (histogram)

Existing push_notification_sent_total and push_notification_token_pruned_total are unchanged.

The channel send throws a typed PushSendError carrying the reason/level/platform, so the top-level loop logs once at the right severity and labels the failure metric without re-deriving anything or double-logging.

Note

Surfacing a push summary in the workflow Metrics tab (email has EmailMetricsSummary; push has no UI at all) is the intended immediate follow-up — this PR is the backend foundation it needs.

How did you test this code?

  • New push-notification-errors.test.ts: parameterized over representative FCM and APNs error shapes, asserting the reason classification, the unregistered flag that drives token pruning, the raw code passthrough, and the severity mapping. Guards against a mapping regression silently breaking pruning or mislabeling metrics.
  • Updated push-notification.service.test.ts to assert the metric_kind: 'push' fix.

Independent verification against this branch (master merged in for a current tree):

  • Ran the normalizer + push service suites: 50/50 passing. tsc --noEmit and prettier --check clean on the changed files.
  • Live end-to-end on a running local stack: registered a device token, triggered a workflow, and confirmed in the Invocations tab that a successful send now logs Push notification accepted by FCM. (the wording changes from "sent via" to "accepted by", since we only know the provider accepted the message, not that the device displayed it).

invocations-tab-push-70913-accepted-by-fcm

The richer provider-error messages (auth rejected, rate-limited, invalid payload) can't be forced against a live provider on demand, so those stay covered by the normalizer unit tests. CI covers the kafka/clickhouse-backed integration suites.

Automatic notifications

  • Publish to changelog?
  • Alert Sales and Marketing teams?

Docs update

No user-facing docs change — this improves the observability surface (workflow logs + internal metrics) with no API or config change.

🤖 Agent context

Autonomy: Human-driven (agent-assisted). Directed by the requester (session owner); left unassigned only because I couldn't verify their GitHub handle this session — a maintainer should set the DRI.

Authored with Claude Code. Two parallel research agents first mapped email's observability end-to-end (metrics/logging/SES webhook) as the target and the CDP metrics/logging infrastructure + current push state, which surfaced the constraints that shaped scope: app-metrics carry no label dimensions (fixed 6-field schema), so platform/reason granularity lives in Prometheus and logs rather than business metrics; and push has no delivery-receipt channel, so open/click parity with email isn't possible. Invoked the /writing-tests skill before writing the normalizer tests to keep them aimed at real regressions. Chose to keep this PR backend-only (fully testable here) and tee up the Metrics-tab UI as a separate follow-up rather than ship unverified frontend wiring.

🤖 Generated with Claude Code


Generated by Claude Code

Push logging and metrics were much thinner than email. On failure the workflow
log just dumped the raw FCM/APNs JSON error at the user, send metrics were
mislabeled as `metric_kind: 'other'` instead of the existing `'push'` kind, and
the only ops signal was a per-platform "sent" counter with no view into why sends
failed.

- Add a provider-agnostic failure-reason normalizer (push-notification-errors.ts)
  that maps FCM error codes / APNs reasons / HTTP status to a small bounded set
  (unregistered, invalid_token, auth_error, rate_limited, invalid_payload,
  provider_error, network_error, unknown) with a plain-language, action-guiding
  message and a severity (config problems the user must fix are errors; transient
  or expected ones are warnings).
- Rewrite the send logs to use it: a readable one-line explanation at the right
  level instead of a raw payload dump; the raw provider response is kept at debug
  level for support. Success reads "accepted by FCM/APNs" (honest — we only know
  the provider took it, not that the device showed it).
- Label ops metrics by reason: new push_notification_failed_total{platform,reason}
  and push_notification_skipped_total{platform}, plus a
  push_notification_send_duration_ms{platform} histogram. Existing sent/pruned
  counters unchanged.
- Emit push business metrics under the correct metric_kind: 'push'.

Delivery/open/click have no equivalent here: FCM/APNs respond synchronously and
there is no delivery-receipt webhook like SES, so this focuses on what we can
actually observe at send time. Surfacing a push summary in the workflow Metrics
tab (email has one, push has none) is the intended follow-up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bbav6vE7Tm24KkdRhZmvNM
@trunk-io

trunk-io Bot commented Jul 14, 2026

Copy link
Copy Markdown

Static BadgeStatic BadgeStatic BadgeStatic Badge

View Full Report ↗︎Docs

@dmarchuk
dmarchuk force-pushed the claude/push-observability branch from 5b29bb7 to 438a5b9 Compare July 15, 2026 10:15
@dmarchuk
dmarchuk marked this pull request as ready for review July 15, 2026 11:11
@pr-assigner-resolver-posthog
pr-assigner-resolver-posthog Bot requested a review from a team July 15, 2026 11:12
@dmarchuk dmarchuk changed the title feat(messaging): richer observability for push notifications feat(workflows): richer observability for push notifications Jul 15, 2026
@greptile-apps

greptile-apps Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Reviews (1): Last reviewed commit: "feat(messaging): richer observability fo..." | Re-trigger Greptile

Comment thread nodejs/src/cdp/services/messaging/push-notification.service.ts
Comment thread nodejs/src/cdp/services/messaging/push-notification-errors.ts Outdated
Addresses two review findings on the push observability change:

- FCM: a bare HTTP 404 was classified as `unregistered` and pruned the device
  token. A generic 404 from a proxy or misconfigured endpoint would then
  permanently drop a still-valid token. Require FCM's own signal (the
  `UNREGISTERED` errorCode or the `NOT_FOUND` canonical status) before pruning;
  an unexplained 404 now surfaces as a failure instead.
- The unregistered/pruned outcome incremented only the token-pruned counter, so
  it was absent from the new reason-labeled skip metric. Give
  push_notification_skipped_total a `reason` label (`no_token` vs
  `unregistered`) and record the prune there too, so the skip metric accounts
  for every non-delivery.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bbav6vE7Tm24KkdRhZmvNM

@mayteio mayteio 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.

Nothing major, solid PR 😎

@dmarchuk
dmarchuk merged commit d72a7eb into master Jul 15, 2026
182 checks passed
@dmarchuk
dmarchuk deleted the claude/push-observability branch July 15, 2026 12:17
@deployment-status-posthog

deployment-status-posthog Bot commented Jul 15, 2026

Copy link
Copy Markdown

Deploy status

Environment Status Deployed At Workflow
dev ✅ Deployed 2026-07-15 12:49 UTC Run
prod-us ✅ Deployed 2026-07-15 13:16 UTC Run
prod-eu ✅ Deployed 2026-07-15 13:15 UTC Run

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.

3 participants