Skip to content

Telemetry: Add sidebar filter telemetry for change detection#34533

Open
valentinpalkovic wants to merge 5 commits intonextfrom
worktree-cuddly-dazzling-hopcroft
Open

Telemetry: Add sidebar filter telemetry for change detection#34533
valentinpalkovic wants to merge 5 commits intonextfrom
worktree-cuddly-dazzling-hopcroft

Conversation

@valentinpalkovic
Copy link
Copy Markdown
Contributor

@valentinpalkovic valentinpalkovic commented Apr 13, 2026

Closes #

What I did

Added a sidebar-filter telemetry event to track usage of built-in tag filters (_docs, _play, _test) and change detection status filters (new, modified, affected) in the sidebar.

The event fires in two scenarios:

  • Interaction: when a user toggles a filter in the sidebar UI
  • URL: when a user visits Storybook with filters pre-set via URL params (shared links/bookmarks)

Each event includes the full active filter state and per-filter story counts. User-defined tag filters are explicitly excluded from telemetry.

Checklist for Contributors

Testing

The changes in this PR are covered in the following automated tests:

  • stories
  • unit tests
  • integration tests
  • end-to-end tests

Manual testing

Caution

This section is mandatory for all contributions. If you believe no manual test is necessary, please state so explicitly. Thanks!

  1. Enable change detection in .storybook/main.ts: features: { changeDetection: true }
  2. Run a sandbox: yarn task --task sandbox --start-from auto --template react-vite/default-ts
  3. Open Storybook, open browser DevTools Network tab
  4. Toggle a status filter (e.g., "New") in the sidebar filter panel → verify a sidebarFilterChanged event is emitted on the channel
  5. Toggle a built-in tag filter (e.g., "Documentation") → verify telemetry event emitted
  6. Toggle a user-defined tag filter → verify NO telemetry event emitted
  7. Visit Storybook with ?statuses=status-value%3Anew URL param → verify a URL-triggered telemetry event fires once on load

Documentation

  • Add or update documentation reflecting your changes
  • If you are deprecating/removing a feature, make sure to update MIGRATION.MD

Checklist for Maintainers

  • When this PR is ready for testing, make sure to add ci:normal, ci:merged or ci:daily GH label to it to run a specific set of sandboxes. The particular set of sandboxes can be found in code/lib/cli-storybook/src/sandbox-templates.ts

  • Make sure this PR contains one of the labels below:

    Available labels
    • bug: Internal changes that fixes incorrect behavior.
    • maintenance: User-facing maintenance tasks.
    • dependencies: Upgrading (sometimes downgrading) dependencies.
    • build: Internal-facing build tooling & test updates. Will not show up in release changelog.
    • cleanup: Minor cleanup style change. Will not show up in release changelog.
    • documentation: Documentation only changes. Will not show up in release changelog.
    • feature request: Introducing a new feature.
    • BREAKING CHANGE: Changes that break compatibility in some way with current major version.
    • other: Changes that don't fit in the above categories.

🦋 Canary release

This PR does not have a canary release associated. You can request a canary release of this pull request by mentioning the @storybookjs/core team here.

core team members can create a canary release here or locally with gh workflow run --repo storybookjs/storybook publish.yml --field pr=<PR_NUMBER>

Summary by CodeRabbit

  • New Features

    • Telemetry for sidebar filter actions and URL-triggered filter state, emitting a "sidebar-filter" event with active filters and story counts.
  • Bug Fixes

    • Avoided false story-selection failures in mobile when sidebar links are hidden.
  • Improvements

    • Consolidated sidebar filter mutation logic and added shared status-counting helpers; telemetry payload computation added for projected filter changes.
  • Tests

    • Added tests for status-counting, telemetry payloads, and telemetry channel handling.

valentinpalkovic and others added 3 commits April 13, 2026 13:19
In mobile mode, clicking a story calls setMobileMenuOpen(false), which
starts a 300ms exit transition on the Modal. After it completes, children
are unmounted (unmountOnExit: true). On slow sandboxes like Angular webpack,
by the time waitForURL resolves the tree is already unmounted, so the
storyLink locator finds nothing and toHaveAttribute fails with
"element(s) not found". URL verification already confirms correct
navigation, so we safely skip the attribute check when the element is gone.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…_CHANGED event

Adds a new channel event emitted when users toggle tag/status filters in the
sidebar (interaction) or load Storybook with filters pre-set via URL params (url).
The telemetry channel forwards the payload to the sidebar-filter event type.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ookup

- stories.ts: extract addFilters/removeFilters helpers to deduplicate the
  identical set-manipulation logic in addTagFilters, addStatusFilters,
  removeTagFilters, and removeStatusFilters
- FilterPanel.tsx: collapse the duplicated just-changed filter lookup in
  emitFilterTelemetry into a single ternary + shared assignment

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 13, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8c42b450-a2e4-4e97-8b2a-15ea449fddba

📥 Commits

Reviewing files that changed from the base of the PR and between b0350cf and ab03ec5.

📒 Files selected for processing (4)
  • code/core/src/core-server/server-channel/telemetry-channel.ts
  • code/core/src/manager/components/sidebar/FilterPanel.tsx
  • code/core/src/manager/components/sidebar/FilterPanel.utils.test.ts
  • code/core/src/manager/components/sidebar/FilterPanel.utils.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • code/core/src/core-server/server-channel/telemetry-channel.ts

📝 Walkthrough

Walkthrough

Adds a new core event SIDEBAR_FILTER_CHANGED and telemetry event 'sidebar-filter'; emits that event during URL-based init and user interactions in the sidebar, wires the server telemetry channel to forward the payload, introduces status-count helpers, refactors filter add/remove logic, and adds tests and a globals export.

Changes

Cohort / File(s) Summary
Core events & globals
code/core/src/core-events/index.ts, code/core/src/manager/globals/exports.ts, code/core/src/telemetry/types.ts
Added and exported SIDEBAR_FILTER_CHANGED; added 'sidebar-filter' to telemetry EventType; exposed new core event in global exports.
Telemetry channel & tests
code/core/src/core-server/server-channel/telemetry-channel.ts, code/core/src/core-server/server-channel/telemetry-channel.test.ts
Telemetry channel now listens for SIDEBAR_FILTER_CHANGED and forwards payload as 'sidebar-filter'; tests verify listener registration and behavior when telemetry enabled/disabled.
Manager API & init logic
code/core/src/manager-api/modules/stories.ts
Refactored repeated filter add/remove logic into shared helpers; on init after fetchIndex() detects built-in URL filters, computes per-filter story counts, and emits SIDEBAR_FILTER_CHANGED with trigger: 'url'.
Sidebar UI & utils
code/core/src/manager/components/sidebar/FilterPanel.tsx, code/core/src/manager/components/sidebar/FilterPanel.utils.ts
Emit SIDEBAR_FILTER_CHANGED for built-in tag/status interactions; added computeFilterTelemetryPayload and FilterTelemetryChanged type; re-exported countStatusesByValue.
Status store & tests
code/core/src/shared/status-store/index.ts, code/core/src/shared/status-store/index.test.ts
Added countStatusesByValue(allStatuses) and fullStatusStore.countByValue() with tests covering counts and edge cases.
Filter data hook
code/core/src/manager/components/sidebar/useFilterData.tsx
Replaced manual status counting with countStatusesByValue for deriving status counts.
FilterPanel utils tests
code/core/src/manager/components/sidebar/FilterPanel.utils.test.ts
Added comprehensive tests for computeFilterTelemetryPayload across tag/status include/exclude/remove actions and payload invariants.
E2E helper
code/e2e-tests/util.ts
Made post-navigation assertion robust to mobile mode by checking sidebar link visibility before asserting data-selected.

Sequence Diagram

sequenceDiagram
    participant User
    participant FilterPanel
    participant StoriesModule
    participant TelemetryChannel
    participant Telemetry

    rect rgba(100, 150, 200, 0.5)
    Note over StoriesModule,Telemetry: URL-based initialization flow
    StoriesModule->>StoriesModule: init() completes fetchIndex()
    StoriesModule->>StoriesModule: Detect built-in filters from URL & compute storyCounts
    StoriesModule->>TelemetryChannel: emit(SIDEBAR_FILTER_CHANGED, {trigger: 'url', ...})
    TelemetryChannel->>Telemetry: emit('sidebar-filter', payload)
    end

    rect rgba(150, 200, 100, 0.5)
    Note over User,Telemetry: User-driven filter interaction
    User->>FilterPanel: Click checkbox / invert
    FilterPanel->>FilterPanel: Compute activeTagFilters, activeStatusFilters, storyCounts
    FilterPanel->>TelemetryChannel: emit(SIDEBAR_FILTER_CHANGED, {action, filterType, ...})
    TelemetryChannel->>Telemetry: emit('sidebar-filter', payload)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
code/core/src/core-server/server-channel/telemetry-channel.test.ts (1)

7-11: Consider using spy: true option for mocks.

Per coding guidelines, Vitest mocks should use the spy: true option. While the current mock implementation works correctly, aligning with the project's mocking conventions would improve consistency.

♻️ Suggested refactor
-vi.mock('storybook/internal/telemetry', () => ({
-  telemetry: vi.fn(),
-  getLastEvents: vi.fn().mockResolvedValue({}),
-  getSessionId: vi.fn().mockResolvedValue('test-session-id'),
-}));
+vi.mock('storybook/internal/telemetry', { spy: true });

Then set up mock implementations in a beforeEach block:

beforeEach(() => {
  vi.mocked(telemetry).mockImplementation(vi.fn());
  vi.mocked(getLastEvents).mockResolvedValue({});
  vi.mocked(getSessionId).mockResolvedValue('test-session-id');
});

As per coding guidelines: "Use vi.mock() with the spy: true option for all package and file mocks in Vitest tests" and "Implement mock behaviors in beforeEach blocks in Vitest tests".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code/core/src/core-server/server-channel/telemetry-channel.test.ts` around
lines 7 - 11, Update the Vitest mocks to use the spy option and move mock
behavior setup into a beforeEach: change the top-level
vi.mock('storybook/internal/telemetry', ...) call to
vi.mock('storybook/internal/telemetry', () => ({ telemetry: vi.fn(),
getLastEvents: vi.fn(), getSessionId: vi.fn() }), { spy: true }), then in the
test file add a beforeEach that uses
vi.mocked(telemetry).mockImplementation(vi.fn()),
vi.mocked(getLastEvents).mockResolvedValue({}), and
vi.mocked(getSessionId).mockResolvedValue('test-session-id') so the mocked
behavior is configured per-test; reference the telemetry, getLastEvents, and
getSessionId symbols when applying these changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@code/core/src/core-server/server-channel/telemetry-channel.test.ts`:
- Around line 7-11: Update the Vitest mocks to use the spy option and move mock
behavior setup into a beforeEach: change the top-level
vi.mock('storybook/internal/telemetry', ...) call to
vi.mock('storybook/internal/telemetry', () => ({ telemetry: vi.fn(),
getLastEvents: vi.fn(), getSessionId: vi.fn() }), { spy: true }), then in the
test file add a beforeEach that uses
vi.mocked(telemetry).mockImplementation(vi.fn()),
vi.mocked(getLastEvents).mockResolvedValue({}), and
vi.mocked(getSessionId).mockResolvedValue('test-session-id') so the mocked
behavior is configured per-test; reference the telemetry, getLastEvents, and
getSessionId symbols when applying these changes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 69ff6dd0-f974-451b-a522-cd49e80193e0

📥 Commits

Reviewing files that changed from the base of the PR and between 0521e2d and 3da50fc.

📒 Files selected for processing (8)
  • code/core/src/core-events/index.ts
  • code/core/src/core-server/server-channel/telemetry-channel.test.ts
  • code/core/src/core-server/server-channel/telemetry-channel.ts
  • code/core/src/manager-api/modules/stories.ts
  • code/core/src/manager/components/sidebar/FilterPanel.tsx
  • code/core/src/manager/globals/exports.ts
  • code/core/src/telemetry/types.ts
  • code/e2e-tests/util.ts

@nx-cloud
Copy link
Copy Markdown

nx-cloud bot commented Apr 13, 2026

View your CI Pipeline Execution ↗ for commit ab03ec5

Command Status Duration Result
nx run-many -t compile,check,knip,test,lint,fmt... ✅ Succeeded 8m 46s View ↗

☁️ Nx Cloud last updated this comment at 2026-04-13 14:43:13 UTC

…ounting loops

Add a canonical countStatusesByValue(allStatuses) helper to the shared
status-store module, expose it as fullStatusStore.countByValue(), and
replace the three independent status-counting loops in useStatusFilterEntries,
stories.ts telemetry init, and FilterPanel.utils.ts with calls to this
single implementation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@code/core/src/manager-api/modules/stories.ts`:
- Around line 1272-1277: The loop over targetStatuses should emit a count of 0
for active status filters that have no matches; update the logic in the block
that reads targetStatuses, statusCounts (from fullStatusStore.countByValue())
and storyCounts so that for each statusValue you assign storyCounts[statusValue]
= statusCounts[statusValue] ?? 0 (or equivalent check for undefined) instead of
skipping when undefined, ensuring every active filter in targetStatuses is
present in storyCounts.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b9f4282f-0b7e-4d74-b0c4-69cdd7bfbf73

📥 Commits

Reviewing files that changed from the base of the PR and between 3da50fc and b0350cf.

📒 Files selected for processing (5)
  • code/core/src/manager-api/modules/stories.ts
  • code/core/src/manager/components/sidebar/FilterPanel.utils.ts
  • code/core/src/manager/components/sidebar/useFilterData.tsx
  • code/core/src/shared/status-store/index.test.ts
  • code/core/src/shared/status-store/index.ts
✅ Files skipped from review due to trivial changes (1)
  • code/core/src/manager/components/sidebar/FilterPanel.utils.ts

Comment on lines +1272 to +1277
const targetStatuses = new Set([...initialIncludedStatuses, ...initialExcludedStatuses]);
const statusCounts = fullStatusStore.countByValue();
for (const statusValue of targetStatuses) {
if (statusCounts[statusValue] !== undefined) {
storyCounts[statusValue] = statusCounts[statusValue];
}
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.

⚠️ Potential issue | 🟠 Major

Emit 0 for active status filters with no matches.

fullStatusStore.countByValue() only includes statuses that currently exist in the store. With the current guard, an active URL status filter that matches zero stories disappears from storyCounts entirely, so the emitted payload no longer contains a per-filter count for every active status filter.

Suggested patch
-        for (const statusValue of targetStatuses) {
-          if (statusCounts[statusValue] !== undefined) {
-            storyCounts[statusValue] = statusCounts[statusValue];
-          }
-        }
+        for (const statusValue of targetStatuses) {
+          storyCounts[statusValue] = statusCounts[statusValue] ?? 0;
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const targetStatuses = new Set([...initialIncludedStatuses, ...initialExcludedStatuses]);
const statusCounts = fullStatusStore.countByValue();
for (const statusValue of targetStatuses) {
if (statusCounts[statusValue] !== undefined) {
storyCounts[statusValue] = statusCounts[statusValue];
}
const targetStatuses = new Set([...initialIncludedStatuses, ...initialExcludedStatuses]);
const statusCounts = fullStatusStore.countByValue();
for (const statusValue of targetStatuses) {
storyCounts[statusValue] = statusCounts[statusValue] ?? 0;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code/core/src/manager-api/modules/stories.ts` around lines 1272 - 1277, The
loop over targetStatuses should emit a count of 0 for active status filters that
have no matches; update the logic in the block that reads targetStatuses,
statusCounts (from fullStatusStore.countByValue()) and storyCounts so that for
each statusValue you assign storyCounts[statusValue] = statusCounts[statusValue]
?? 0 (or equivalent check for undefined) instead of skipping when undefined,
ensuring every active filter in targetStatuses is present in storyCounts.

…lemetry

Extract computeFilterTelemetryPayload() as a pure helper that applies
the pending action to the current filter arrays before emitting, so the
SIDEBAR_FILTER_CHANGED payload always reflects the state after the
toggle rather than the stale pre-change React state.

Also removes the partial workaround that only fixed storyCounts but left
activeTagFilters/activeStatusFilters stale, and drops the unnecessary
async keyword from the telemetry-channel handler.

Adds 19 unit tests covering all action/filterType combinations.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant