Summary
Replace the "Anomaly alerts" README TODO with something broader: a Daily Pulse email per brand — a short digest sent after the daily tracking run that covers both the good news and the warnings, instead of an alerts-only "bad news machine". Anomaly detection becomes one section of the pulse.
Naming: the feature is deliberately named Pulse, not Brief or Report — brief collides with the existing Content Brief feature and report with the Reports feature, which would make the codebase painful to grep. Use pulse consistently: pulse_settings, sent_pulses, daily_pulse.created.
Cloud-only: the email leg is gated behind isCloud() — self-hosted deployments do not send Daily Pulse emails. The daily_pulse.created webhook event ships through the existing per-brand webhook system and works everywhere.
V1 scope
Content (per brand, generated after the daily tracking run):
- KPI strip — visibility rate (yesterday + 7-day trend), mentions, citations, sentiment. Reuse the same RPCs the Insights page uses (
insights_aggregates, competitor_aggregates, prompt_visibility_summaries) so the email and the dashboard always agree.
- Highlights (when present) — first-time citations (
prompt_target_urls.first_cited_at), top 2–3 prompts by visibility gain, a competitor overtaken on the leaderboard, first appearance on a new answer engine.
- Warnings (when present) — three detectors:
- Sharp visibility drop: 7-day rate vs previous 7 days; alert when the drop is ≥15 points and ≥30% relative, with a minimum data floor (≥10 prompts with results).
- Competitor surge: a competitor's rate up ≥15 points week-over-week, or crossing above the brand's rate for the first time.
- High-volume prompt lost citations: a prompt with volume data that was cited in ≥60% of runs over the last 14 days drops to 0 citations for 3 consecutive runs.
- Platform-outage guard: if a platform's results collapse across all orgs on a given day (data-collection incident, e.g. a scraper format change), exclude that platform from detection or annotate the warning — never mass-email "your visibility dropped" for our own pipeline issue.
- CTA — link to the brand's Insights page.
Delivery:
- Email via Resend, cloud only — new small
server email module using the Resend HTTP API (RESEND_API_KEY), sending gated behind isCloud().
- One digest per brand per day (never one email per event). Include a
List-Unsubscribe header and a settings link.
- Webhook — add a
daily_pulse.created event to the existing per-brand webhook_configs system (HMAC envelope already in place), so Slack/Notion delivery works from day one through webhooks — including on self-host; native Slack is v2.
Settings (Settings → Notifications, per brand):
- Frequency: Daily / Weekly / Only when something notable / Off (default Daily). Weekly runs the same engine over a 7-day window.
- Recipient list (default: organization members).
Eligibility — hard requirement:
- Send only to organizations whose subscription is active or trialing (the existing
isSubscriptionActive check: active / trialing).
- Users whose trial has expired, or whose subscription has lapsed/canceled, must never receive these emails — no re-engagement mail from this feature.
- Skip brands with no fresh tracking results for the day.
Data model:
pulse_settings (per brand: frequency, recipients) and sent_pulses (send log — dedup, "already sent today" check, and warning cooldown: don't repeat the same warning for the same subject within 7 days).
Out of scope (v2)
- AI-generated narrative summary (reuse the Reports AI-summary mechanism; LLM cost is per brand × day, so plan-gate it).
- Native Slack incoming-webhook field with Block Kit formatting.
- In-app notification center; per-detector threshold tuning UI.
Acceptance criteria
- After the daily cron, each eligible brand's recipients get at most one email; content matches the dashboard numbers for the same window.
- No email is ever sent to an org that is not
active/trialing, and none are sent from self-hosted deployments (isCloud() gate).
- The
daily_pulse.created webhook event fires on both cloud and self-host when a pulse is generated.
- The README TODO item "Anomaly alerts" is updated to reference this feature in the implementing PR.
Summary
Replace the "Anomaly alerts" README TODO with something broader: a Daily Pulse email per brand — a short digest sent after the daily tracking run that covers both the good news and the warnings, instead of an alerts-only "bad news machine". Anomaly detection becomes one section of the pulse.
V1 scope
Content (per brand, generated after the daily tracking run):
insights_aggregates,competitor_aggregates,prompt_visibility_summaries) so the email and the dashboard always agree.prompt_target_urls.first_cited_at), top 2–3 prompts by visibility gain, a competitor overtaken on the leaderboard, first appearance on a new answer engine.Delivery:
serveremail module using the Resend HTTP API (RESEND_API_KEY), sending gated behindisCloud().List-Unsubscribeheader and a settings link.daily_pulse.createdevent to the existing per-brandwebhook_configssystem (HMAC envelope already in place), so Slack/Notion delivery works from day one through webhooks — including on self-host; native Slack is v2.Settings (Settings → Notifications, per brand):
Eligibility — hard requirement:
isSubscriptionActivecheck:active/trialing).Data model:
pulse_settings(per brand: frequency, recipients) andsent_pulses(send log — dedup, "already sent today" check, and warning cooldown: don't repeat the same warning for the same subject within 7 days).Out of scope (v2)
Acceptance criteria
active/trialing, and none are sent from self-hosted deployments (isCloud()gate).daily_pulse.createdwebhook event fires on both cloud and self-host when a pulse is generated.