diff --git a/packages/design-system/src/components/FeedbackPulse/ces-survey-architecture.md b/packages/design-system/src/components/FeedbackPulse/ces-survey-architecture.md new file mode 100644 index 00000000..47d02dc6 --- /dev/null +++ b/packages/design-system/src/components/FeedbackPulse/ces-survey-architecture.md @@ -0,0 +1,217 @@ +# Feedback Pulse — Architecture + +## Overview +A popover-style component for collecting Customer Effort Score (CES) feedback on newly released features. Uses a 1–5 numeric scale with WADS `toggle-button` instances, and an optional free-form textarea that reveals after rating selection. + +## Visual References +- **Primary**: Loom — numeric scale, labels at ends, textarea + submit +- **Secondary**: Vercel — clean, calm, professional feel + +--- + +## Component Name +`feedback-pulse` + +## Variant Properties + +| Property | Type | Options | Default | +|----------|---------|-------------------------------------|-----------| +| State | VARIANT | `Rating`, `Feedback`, `Submitted` | `Rating` | + +## Component Properties (following WADS `Name#id:id` convention) + +| Property | Type | Default | Notes | +|-----------|---------|--------------------------------------------|-------------------------------------------------| +| Question | TEXT | "How easy was it to use this feature?" | Configurable per feature. Alternative wording: "How did we do?" with updated labels | +| Textarea | BOOLEAN | true | Visible in Feedback state, toggles textarea visibility | + +--- + +## States + +### State = Rating (initial) +User sees the question and 5 numbered buttons. No textarea yet. + +``` +┌─────────────────────────────────────┐ +│ How easy was it to use [X] │ +│ this feature? │ +│ │ +│ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ │ +│ │ 1 │ │ 2 │ │ 3 │ │ 4 │ │ 5 │ │ +│ └───┘ └───┘ └───┘ └───┘ └───┘ │ +│ Very difficult Very easy │ +└─────────────────────────────────────┘ +``` + +### State = Feedback (after number selected) +One number is shown as selected (toggle-button Selected=On). Textarea and send button appear below. + +``` +┌─────────────────────────────────────┐ +│ How easy was it to use [X] │ +│ this feature? │ +│ │ +│ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌═══┐ │ +│ │ 1 │ │ 2 │ │ 3 │ │ 4 │ ║ 5 ║ │ +│ └───┘ └───┘ └───┘ └───┘ └═══┘ │ +│ Very difficult Very easy │ +│ │ +│ ┌─────────────────────────────┐ │ +│ │ Tell us why? (optional) │ │ +│ │ │ │ +│ └─────────────────────────────┘ │ +│ [ Send ] │ +└─────────────────────────────────────┘ +``` + +### State = Submitted (confirmation, auto-dismissible) +Compact confirmation with timeout progress bar and close button. Styled as a light popover card with an auto-dismiss progress indicator (left-to-right fill rectangle). + +``` +┌─────────────────────────────────────┐ +│ ██░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │ ← timeout bar (absolute, stretches height) +│ ✓ Thanks a lot! — Wallarm Team [X]│ +└─────────────────────────────────────┘ +``` + +--- + +## Existing WADS Components Used (instances, not recreated) + +| Element | WADS Component | Variant / Config | Key | +|----------------------|-----------------------------------|----------------------------------------------------------------------------|--------------------------------------------| +| Number buttons | `toggle-button` | Type=Outline, Color=Neutral, Size=Small, Icon only=Off, Left element=Off | `c63426f6cb0e8b72b71fe41b50bfcf07cc822e28` | +| Selected number | `toggle-button` | Same but Selected=On | (same set) | +| Textarea | `text-area` | Size=Default, Error=Off, Label=off, Description=off, Counter=off, Help text=off | `d69a6f9aa19f7433b32798f52f0ee244ea4b667a` | +| Send button | `Button` | Type=Primary, Color=Brand, Size=Medium, Left element=Off, Label="Send" | `9bc0755394bc26d3a759c452a0f06b5e9e16a891` | +| Close button (Rating/Feedback) | `Button` | Type=Ghost, Color=Neutral, Size=Small, Icon only=On, icon swapped to `x` | `9bc0755394bc26d3a759c452a0f06b5e9e16a891` | +| Close button (Submitted) | Internal button frame | Absolute positioned, constraints: horizontal=MAX, vertical=CENTER | — | +| Check icon | `checkbox-check` (WADS Iconography) | 16×16, fill: `color-icon/success` | `c829f698d49e8daa0cdae4f8a212defba1575a02` | +| Close icon | `x` (WADS Iconography) | — | `35ddd651f5d0613dbec7d873d382be09d57a18fe` | + +--- + +## Card Container Specs (matching WADS `popover` component) + +All values below are sourced from the existing WADS `popover` component (`23df37f4c6de782e015675af6c0ef5396c56f7e9`). + +| Property | Token | Resolved Value | +|----------------|-----------------------------------|----------------| +| Background | `color-bg/surface-2` | white | +| Border color | `color-border/primary-light` | light grey | +| Border width | `Border-width/border-1` | 1px | +| Corner radius | `radius-12` | 12px | +| Padding | `spacing-12` (all sides) | 12px | +| Item spacing | `spacing-8` | 8px | +| Shadow | Popover shadow effect style | 2-layer drop shadow (0 2 4 black/10%, 0 4 6 black/10%) | +| Width | **400px fixed** | not responsive | +| Clip content | `true` | prevents child shadow overflow | + +> Effect style ID from popover: `S:934163a1d21e159b7fe98255ab5992bf8b3b3238,2398:1020` + +--- + +## Layer Structure + +``` +feedback-pulse (COMPONENT_SET, width=400 fixed) +│ +├── State=Rating (COMPONENT, clipsContent=true) +│ ├── header (FRAME, horizontal, space-between, gap=spacing-8) +│ │ ├── Question (TEXT, configurable) +│ │ │ ├── Style: heading/sm/medium +│ │ │ └── Color: color-text/primary +│ │ └── close-button (INSTANCE — WADS Button Ghost/Neutral/Small/Icon only, x icon) +│ └── scale-group (FRAME, vertical, gap=spacing-4) +│ ├── scale-row (FRAME, horizontal, gap=spacing-8) +│ │ ├── toggle-button "1" (INSTANCE — Selected=Off, FILL width) +│ │ ├── toggle-button "2" (INSTANCE — Selected=Off, FILL width) +│ │ ├── toggle-button "3" (INSTANCE — Selected=Off, FILL width) +│ │ ├── toggle-button "4" (INSTANCE — Selected=Off, FILL width) +│ │ └── toggle-button "5" (INSTANCE — Selected=Off, FILL width) +│ └── scale-labels (FRAME, horizontal, space-between) +│ ├── "Very difficult" (TEXT — text/sm/regular, color-text/secondary) +│ └── "Very easy" (TEXT — text/sm/regular, color-text/secondary) +│ +├── State=Feedback (COMPONENT, clipsContent=true) +│ ├── header (same as Rating) +│ ├── scale-group (same structure, but toggle-button "5" has Selected=On) +│ ├── text-area (INSTANCE — WADS text-area, all labels off) +│ │ └── Placeholder: "Tell us why? (optional)" +│ └── footer (FRAME, horizontal, align-right) +│ └── submit-button (INSTANCE — WADS Button Primary/Brand/Medium) +│ └── Text: "Send" +│ +└── State=Submitted (COMPONENT, clipsContent=true, fixed height=48px) + ├── timeout (RECTANGLE, absolute positioned) + │ ├── Fill: color-component/bg-input-default + │ └── Constraints: horizontal=STRETCH, vertical=STRETCH + ├── check-icon (INSTANCE — checkbox-check, 16×16) + │ └── Fill: color-icon/success + ├── confirmation (TEXT) + │ ├── Content: "Thanks a lot! — Wallarm Team" + │ ├── Style: Geist Medium 14px (font vars bound) + │ ├── Color: color-text/primary + │ └── Layout: FILL width (grows to fill) + └── close-button (FRAME + x icon instance, absolute positioned) + ├── Constraints: horizontal=MAX, vertical=CENTER + └── Icon fill: color-icon/primary +``` + +--- + +## Token Usage Summary (zero raw values) + +### Spacing +| Usage | Token | +|---------------------------------|----------------| +| Card padding (all sides) | `spacing-12` | +| Card item spacing | `spacing-8` | +| Scale group gap (numbers→labels)| `spacing-4` | +| Scale row gap (between numbers) | `spacing-8` | +| Header gap | `spacing-8` | + +### Typography +| Element | Text Style | Color Variable | +|------------------|-----------------------|-------------------------| +| Question | `heading/sm/medium` | `color-text/primary` | +| Scale labels | `text/sm/regular` | `color-text/secondary` | +| Confirmation | Geist Medium 14px (font variables bound) | `color-text/primary` | +| Textarea hint | (inherited from WADS text-area component) | — | + +### Colors +| Element | Variable | +|------------------------|----------------------------------| +| Card background | `color-bg/surface-2` | +| Card border color | `color-border/primary-light` | +| Card border width | `Border-width/border-1` | +| Card radius | `radius-12` | +| Check icon | `color-icon/success` | +| Close icon (Submitted) | `color-icon/primary` | +| Timeout bar | `color-component/bg-input-default` | + +### Effects +| Element | Style | +|-------------|--------------------------------| +| Card shadow | Popover shadow effect style (`S:934163a1d21e159b7fe98255ab5992bf8b3b3238`) | + +--- + +## Close Button Approach + +**Rating & Feedback states**: Use WADS `Button` component with `Type=Ghost, Color=Neutral, Size=Small, Icon only=On`, swapping the icon to the WADS `x` icon. This keeps it fully within the design system. + +**Submitted state**: Uses a custom frame wrapper (8px padding, radius-8) with the WADS `x` icon instance inside. Absolute positioned with `horizontal=MAX, vertical=CENTER` constraints to pin it to the right center of the card. Icon color uses `color-icon/primary`. + +--- + +## Interaction Notes +- Toggle buttons in scale-row use `Left element=false` — showing just the number text, no icons +- Each toggle-button has `layoutSizingHorizontal = FILL` to distribute evenly across the row +- In the Feedback state, exactly one toggle-button shows `Selected=On` to represent the chosen rating (button "5" selected as the default showcase) +- The textarea is optional (controlled by `Textarea` boolean property) — if hidden, the Feedback state just shows the send button +- The Submitted state has a timeout progress bar (absolute rectangle) that represents auto-dismiss progress (left-to-right fill). In code, animate its width from 0 to 100% over the dismiss duration +- The Submitted state uses `clipsContent=true` with fixed height (48px) to contain the timeout bar +- Question text is configurable — alternative wording "How did we do?" can be used with labels "Not well" / "Very well" +- Scale labels could be made configurable TEXT properties in a future iteration if needed diff --git a/packages/design-system/src/components/FeedbackPulse/feedback-pulse-data-ops.md b/packages/design-system/src/components/FeedbackPulse/feedback-pulse-data-ops.md new file mode 100644 index 00000000..60af2a52 --- /dev/null +++ b/packages/design-system/src/components/FeedbackPulse/feedback-pulse-data-ops.md @@ -0,0 +1,259 @@ +# Feedback Pulse — Data Storage & Operations + +## Overview + +Where the feedback goes, how it's stored, who gets notified, and how to act on it. + +--- + +## Architecture: Three Layers + +``` +User submits feedback + │ + ▼ +┌─────────────────┐ +│ 1. DATABASE │ ← source of truth, every response stored +│ (PostgreSQL) │ +└────────┬────────┘ + │ + ┌────┴────┐ + ▼ ▼ +┌────────┐ ┌──────────────┐ +│2. SLACK│ │ 3. JIRA │ +│ (live │ │ (actionable │ +│ feed) │ │ items only) │ +└────────┘ └──────────────┘ +``` + +Each layer has a different purpose. Not everything goes everywhere. + +--- + +## Layer 1: Database (Source of Truth) + +### What to store + +Every single response, no exceptions. This is your analytics source. + +**Table: `feedback_pulse_responses`** + +| Column | Type | Description | +|---|---|---| +| `id` | UUID | Primary key | +| `feature_id` | VARCHAR | Identifies the feature release (e.g., `filtering-v2`, `dashboard-threats`) | +| `feature_name` | VARCHAR | Human-readable feature name ("Advanced Filtering") | +| `score` | INTEGER | 1–5 CES rating | +| `comment` | TEXT | Free-form text, nullable | +| `user_id` | VARCHAR | Internal user ID | +| `user_email` | VARCHAR | For follow-up if needed | +| `account_id` | VARCHAR | Company/tenant ID | +| `account_name` | VARCHAR | Company name | +| `plan` | VARCHAR | Free / Pro / Enterprise — useful for segmenting | +| `role` | VARCHAR | User's role (admin, viewer, etc.) | +| `created_at` | TIMESTAMP | When the response was submitted | +| `session_url` | VARCHAR | Link to session replay if available (FullStory, LogRocket, etc.) | +| `page_url` | VARCHAR | Where the pulse was shown | +| `trigger_event` | VARCHAR | What action triggered the pulse | +| `time_to_respond_ms` | INTEGER | Time between pulse shown and rating submitted | +| `dismissed` | BOOLEAN | True if user closed without rating | +| `environment` | VARCHAR | Production / staging | + +**Table: `feedback_pulse_config`** + +| Column | Type | Description | +|---|---|---| +| `feature_id` | VARCHAR | Primary key | +| `feature_name` | VARCHAR | Human-readable name | +| `question_text` | VARCHAR | The question shown to users | +| `enabled` | BOOLEAN | Kill switch per feature | +| `started_at` | TIMESTAMP | When the pulse started showing | +| `ended_at` | TIMESTAMP | When to stop showing (auto-expire) | +| `target_responses` | INTEGER | Stop after N responses (optional cap) | +| `cooldown_days` | INTEGER | Override global cooldown if needed | + +### Querying + +Common queries the team will need: + +```sql +-- Average score per feature +SELECT feature_id, feature_name, + AVG(score) as avg_score, + COUNT(*) as responses +FROM feedback_pulse_responses +WHERE dismissed = false +GROUP BY feature_id, feature_name +ORDER BY created_at DESC; + +-- Low scores with comments (action items) +SELECT * FROM feedback_pulse_responses +WHERE score <= 2 AND comment IS NOT NULL +ORDER BY created_at DESC; + +-- Response rate per feature +SELECT feature_id, + COUNT(*) FILTER (WHERE dismissed = false) as rated, + COUNT(*) FILTER (WHERE dismissed = true) as dismissed, + ROUND(100.0 * COUNT(*) FILTER (WHERE dismissed = false) / COUNT(*), 1) as response_rate +FROM feedback_pulse_responses +GROUP BY feature_id; +``` + +--- + +## Layer 2: Slack (Live Feed) + +### Purpose +Real-time visibility. The team sees feedback as it comes in — no need to check a dashboard. + +### Setup +- Create a dedicated channel: **`#feedback-pulse`** +- Bot posts every response in near real-time (via webhook or Slack app) + +### Message Format + +**For ratings with comments:** +``` +🟢 Feedback Pulse — Advanced Filtering + +Score: ★★★★☆ (4/5) +Comment: "Love the new date range picker, but the tag selector is confusing" + +User: jane@acme.com (Acme Corp, Enterprise) +Submitted: 2026-03-26 14:32 UTC +``` + +**For ratings without comments:** +``` +🟡 Feedback Pulse — Advanced Filtering + +Score: ★★★☆☆ (3/5) +No comment + +User: john@startup.io (Startup Inc, Pro) +Submitted: 2026-03-26 14:45 UTC +``` + +**For low scores (1–2), make it loud:** +``` +🔴 Low Score Alert — Advanced Filtering + +Score: ★☆☆☆☆ (1/5) +Comment: "Completely broken on Safari, filters reset every time I switch tabs" + +User: mike@bigcorp.com (BigCorp, Enterprise) +Submitted: 2026-03-26 15:01 UTC + +→ Jira ticket auto-created: FP-42 +``` + +### Slack Rules +- All responses go to `#feedback-pulse` +- Scores 1–2 also get cross-posted to `#product-alerts` (or wherever urgent product issues go) +- Weekly summary digest posted every Monday: avg score, total responses, top comments + +--- + +## Layer 3: Jira (Actionable Items Only) + +### Purpose +Not every response needs a ticket. Only create Jira issues for feedback that requires action. + +### Project Setup +- **Project key**: `FP` (Feedback Pulse) +- **Issue type**: Task +- **Default assignee**: Product Owner or PM for the feature area + +### Auto-Create Ticket When: +1. **Score is 1 or 2** AND **comment is not empty** — something is clearly wrong, with context +2. **Score is 1 or 2** AND **account is Enterprise tier** — high-value customer friction, even without comment + +### Do NOT Auto-Create Ticket When: +- Score is 3+ (that's decent — analyze in aggregate, not per-ticket) +- Score is 1–2 but no comment (nothing actionable — track in aggregate) +- Duplicate — same user, same feature, same week + +### Ticket Template + +``` +Title: [FP] {feature_name} — Score {score}/5 from {account_name} + +Description: +Feature: {feature_name} ({feature_id}) +Score: {score}/5 +Comment: "{comment}" + +User: {user_email} +Account: {account_name} ({plan}) +Page: {page_url} +Submitted: {created_at} + +--- +Auto-created by Feedback Pulse. +Review and triage within 48 hours. +``` + +### Labels +- `feedback-pulse` +- `score-1` or `score-2` +- Feature-specific label (e.g., `filtering`, `dashboard`) + +### Workflow +1. Ticket created → lands in **Triage** column +2. PM reviews within 48 hours +3. Either: + - **Link to existing issue** if it's a known problem + - **Create follow-up** if it's a new insight + - **Close as noted** if it's subjective/not actionable + +--- + +## Weekly Review Ritual + +Every Monday, auto-generate and post to `#feedback-pulse`: + +``` +📊 Feedback Pulse — Weekly Summary (Mar 19–26) + +Features active: 3 +Total responses: 47 +Overall avg score: 3.8/5 + +Feature breakdown: +• Advanced Filtering: 4.1/5 (23 responses) ✅ +• Threat Dashboard: 3.2/5 (18 responses) ⚠️ +• API Sessions v2: 4.5/5 (6 responses) ✅ + +Top comments (most frequent themes): +1. "Filtering is fast but tag selector needs work" (×4) +2. "Dashboard loading time is noticeable" (×3) + +Action items created: 2 Jira tickets +``` + +This can be a scheduled script that queries the database and posts to Slack. + +--- + +## Privacy & Retention + +- **No PII in Slack messages beyond email** — no session recordings, no IP addresses +- **Jira tickets**: include email for follow-up context, but not full user metadata +- **Database retention**: keep responses for 12 months, then anonymize (remove user_email, replace user_id with hash) +- **GDPR**: if a user requests data deletion, remove their entries from `feedback_pulse_responses` +- **Opt-out**: respect any future "disable in-app surveys" user setting + +--- + +## Implementation Priority + +| Phase | What | Effort | +|---|---|---| +| **Phase 1** | Database table + API endpoint to receive responses | Backend, ~2 days | +| **Phase 2** | Slack webhook integration (post every response) | Backend, ~1 day | +| **Phase 3** | Jira auto-creation for low scores | Backend, ~1 day | +| **Phase 4** | Weekly summary script | Backend, ~0.5 day | +| **Phase 5** | Config table + admin UI to enable/disable per feature | Full-stack, ~2 days | + +Total: ~6.5 dev days for the full pipeline. Phase 1 + 2 gets you 80% of the value. diff --git a/packages/design-system/src/components/FeedbackPulse/feedback-pulse-framework.md b/packages/design-system/src/components/FeedbackPulse/feedback-pulse-framework.md new file mode 100644 index 00000000..dfba973f --- /dev/null +++ b/packages/design-system/src/components/FeedbackPulse/feedback-pulse-framework.md @@ -0,0 +1,169 @@ +# Feedback Pulse — When & How to Use + +## Guiding Principle + +Show the pulse **once**, at the **right moment**, for **meaningful releases only**. The user should feel like we genuinely care about their opinion — not like we're pestering them. + +--- + +## What Qualifies for a Feedback Pulse + +### Show it for: +- **New pages or views** — new dashboard, new report screen, new settings section +- **New workflows** — new filtering flow, new rule creation wizard, new onboarding sequence +- **Major feature additions** — new attack type detection, new integration, new API capability +- **Significant UX overhauls** — redesigned navigation, reworked data tables, new visualization type + +### Do NOT show it for: +- Bug fixes +- Copy/text changes +- Performance improvements (invisible to user) +- Minor UI tweaks (icon swap, color adjustment, spacing fix) +- Backend-only changes +- Incremental iterations on already-pulsed features + +### Grey area (use judgment): +- Feature enhancements that meaningfully change how the user interacts with an existing flow — yes, if the change is noticeable +- New settings or configuration options — only if it's a standalone experience, not just a new toggle + +**Rule of thumb**: if the release has its own changelog entry with a title and description, it probably qualifies. If it's a bullet point under "improvements" — it doesn't. + +--- + +## When to Trigger + +### The Moment + +Show the feedback pulse after the **first successful completion** of the new feature's core action. Not on page load. Not on hover. After the user has actually done the thing. + +| Feature type | Trigger moment | +|---|---| +| New filtering flow | User applies a filter and sees results | +| New dashboard | User has been on the dashboard for 15+ seconds (enough to scan) | +| New rule creation | User successfully saves a new rule | +| New integration | User completes the integration setup | +| New report/view | User interacts with the content (scroll, click, expand) | + +### Why "after first success" +- The user has enough context to give meaningful feedback +- They've experienced the value (or lack of it) +- It doesn't interrupt their task — they just completed something +- It feels natural, not intrusive + +### Positioning +- Bottom-right corner of the viewport, floating above content +- Appears with a subtle slide-up animation +- Does not block any UI elements or CTAs + +--- + +## Frequency Rules + +### Per feature +- Show **once per user per feature release** +- If the user dismisses (X) without rating — do not show again for this feature +- If the user rates but doesn't submit text — that's fine, count it as complete +- If the user submits — done, never show again for this feature + +### Global cooldown +- **Maximum 1 feedback pulse per user per 7 days** +- If multiple features ship in the same week, prioritize the most impactful one +- Queue others for the following week (if still relevant) + +### Session rules +- Do not show on the user's first session ever (they're still orienting) +- Do not show within the first 2 minutes of a session (let them settle in) +- Do not show if the user is in the middle of a multi-step flow (wait until completion) +- Do not show if another modal, dialog, or popover is already visible + +--- + +## User Segments + +### Who sees it +- All active users who have access to the new feature +- Both free and paid tiers (feedback from both is valuable) + +### Who doesn't see it +- Users who haven't logged in since the feature shipped (they haven't used it) +- Users whose role doesn't have access to the feature +- Users who have opted out of in-app surveys (if such a setting exists) + +--- + +## Lifecycle of a Feedback Pulse + +``` +Feature ships + │ + ▼ +User accesses the feature area + │ + ▼ +User completes core action (first success) + │ + ├── Cooldown check: was a pulse shown in the last 7 days? + │ │ + │ YES → Queue for later / skip + │ │ + │ NO ──▼ + │ + ▼ +Show feedback-pulse (State=Rating) + │ + ├── User clicks X → Dismiss. Mark as "declined". Do not show again for this feature. + │ + ├── User selects a number → Transition to State=Feedback + │ │ + │ ├── User types feedback + clicks Send → Submit. Transition to State=Submitted (auto-dismiss). + │ │ + │ ├── User clicks Send without typing → Submit rating only. Transition to State=Submitted. + │ │ + │ └── User clicks X → Submit rating only (they already voted). Mark as complete. + │ + └── User ignores it (no interaction for 30 seconds) → Auto-dismiss quietly. Mark as "seen, no action". + Do not show again for this feature. +``` + +--- + +## Data to Capture + +| Field | Type | Required | +|---|---|---| +| `feature_id` | string | yes — identifies which release | +| `score` | integer (1–5) | yes — the CES rating | +| `comment` | string | no — free-form text | +| `user_id` | string | yes — who gave feedback | +| `timestamp` | datetime | yes | +| `session_context` | string | no — what page/action triggered it | +| `dismissed` | boolean | yes — if user closed without rating | +| `time_to_respond` | integer (ms) | no — how long between showing and rating | + +--- + +## Integration Checklist for Engineers + +When shipping a new feature with a feedback pulse: + +1. **Tag the feature** with a unique `feature_id` in the feedback pulse config +2. **Define the trigger event** — what constitutes "first successful completion" +3. **Set the question text** (or use the default "How easy was it to use this feature?") +4. **Verify cooldown** — ensure the 7-day global cooldown is respected +5. **Test the flow**: trigger → rate → send → submitted → auto-dismiss +6. **Test edge cases**: dismiss, ignore, multiple features queued +7. **Verify analytics** — score + comment are logged correctly + +--- + +## Measuring Success + +### Per feature +- **Response rate**: % of users who saw the pulse and rated (target: >30%) +- **Average CES score**: 1–5 scale (target: >3.5 for new features) +- **Comment rate**: % who also left a text comment (target: >15%) + +### Overall program health +- **Dismiss rate**: if >60% of users dismiss without rating, the pulse is too frequent or poorly timed +- **Opt-out rate**: if users ask to disable surveys, reduce frequency +- **Score trends**: track CES over time per feature to see if iteration improves it