Skip to content

feat(web): Product analytics page: events, funnels and paths - #2172

Open
Makisuo wants to merge 18 commits into
refactor/funnel-step-builder-kitfrom
feat/product-analytics-page
Open

Makisuo wants to merge 18 commits into
refactor/funnel-step-builder-kitfrom
feat/product-analytics-page

Conversation

@Makisuo

@Makisuo Makisuo commented Nov 3, 2026 •

Copy link
Copy Markdown
Collaborator

Product events were only reachable through the dashboard widget builder and one card on Web analytics, so almost nobody found funnels or paths. This adds a Product analytics page at /product-analytics, beside Web analytics under Analyze. Web analytics itself is unchanged apart from a link from its Events card.

  • Events: every event name in the window, with its kind (custom, page view, screen), events, sessions and people. Each row has Funnel and Paths actions. Stat rail: total events and the change vs the previous period, event names by kind, top custom event. When the read hits its 200-name cap, the totals say so instead of showing a trend.
  • Funnels: the shared step builder (refactor(web): move the funnel step builder into the page kit #2171), Count by, Within, and Bars or Drop-off. Then a stat rail (entered, converted, conversion, biggest drop), the existing funnel or drop-off chart, and a steps table with time between steps and what leavers did next. With no steps it suggests pairs built from the org's own custom events.
  • Paths: an anchor event or page, direction, steps and branches. Then the existing paths chart and two tables: the most common next step, and where sequences ended. Each row has "Paths from here".
  • The definition lives in the URL, so links can be shared. Typed step names wait for a 450 ms pause before they run; picks run at once. Every navigation (tab, filter, time range) carries a half-typed step, so nothing typed is lost.
  • The Web analytics filter rail moves to page/kit/analytics-filter-rail.ts and both pages mount it. Product analytics leaves out the Traffic and Event sections, because funnels don't take them.
  • No new queries: it uses getProductEventNames, getProductEventsFunnelWidget, getProductEventsPathsWidget and getWebAnalyticsBreakdowns.
  • /lab/product-analytics?state=… renders every tab and state over fixtures.
  • Preview data: the demo seed (/internal/demo/seed, local and preview stacks only) now also writes about 40 visitors an hour as session_replays rows plus their product_events, walking a signup funnel. Before this, previews had no web or product analytics data at all.
  • Ingest guard: the guard that refuses rows from another org now also reads org_id. That is the ingestion-JSON key of session_replays and product_events; the guard used to refuse them because it saw no org tag. Every tag a row carries must still be the writing org, and the executor test covers a foreign org_id.

Screenshots

Events (dark, light)

events
events light

Funnels, drop-off view (dark, light), and bars view

funnels
funnels light
funnels bars

Paths

paths

Empty, loading, error and mobile states

New funnel with suggestions

funnels new

Failed funnel

funnels error

No events

events empty

Loading

events loading

Paths with no anchor

paths new

Mobile

events mobile
funnels mobile

Verification

  • bunx vitest run src/page/product-analytics src/page/kit src/page/analytics src/page/dashboards src/route src/shell src/lab/registry.test.ts: 302 tests pass. The new scene tests cover:

    • the reads each tab makes;
    • funnel params, with drop-off details on by default;
    • the typing pause, where only the last pause writes;
    • a filter change mid-typing carrying the draft;
    • a row's Funnel replacing a half-typed funnel;
    • the page's own URL write keeping the draft while Back replaces it;
    • rail keys this page ignores;
    • rendering of populated, empty and funnel results.

    Navigations are decoded through the app router, so /product-analytics URLs are covered too.

  • tsrs --noEmit clean; oxlint -c .oxlintrc.effect.json, ui:index:check and ui:patterns:check pass.

  • In a browser (Playwright on /lab/product-analytics): every state above, dark and light, desktop and mobile. Step comboboxes open with the window's event names and counts, and picking one fills the step.

  • Preview: a separate agent tested app-pr-2172 against the seeded data. Every tab, the filter rail and the Web analytics link worked, with no console errors and no failed requests. Results and traces are in the comments.

  • Not tested on the preview: the dashboard funnel editor (refactor(web): move the funnel step builder into the page kit #2171). Dashboards need live sync, which preview:web doesn't run. refactor(web): move the funnel step builder into the page kit #2171's scene tests drive the editor's combobox and kind select.

Follow-up: "Add to dashboard" from a funnel or paths view.

Stack: #2171 → this PR

Product events were only reachable through the dashboard widget builder
and one tab on Web analytics. /product-analytics gives them their own page
under Analyze, beside Web analytics:

- Events: every event name in the window with its kind, counts and reach,
  and a Funnel and Paths action per row.
- Funnels: the kit step builder, Count by / Within / Bars or Drop-off, a
  stat rail, the funnel chart and a steps table with time between steps and
  what leavers did. With no steps it suggests pairs from the org's events.
- Paths: an anchor event or page, direction, steps and branches, the paths
  chart, and the most common next step and where sequences ended.

The definition lives in the URL (shareable); typed step names wait for a
pause before they run. The Web analytics filter rail moves to the kit and
both pages mount it. Web analytics' Events card links to the new page.
/lab/product-analytics renders every tab and state over fixtures.
@coderabbitai

coderabbitai Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

Run configuration

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 6ea12197-e80d-48ee-b702-766c2697f0b8

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@Makisuo Makisuo added the preview:web Deploy a Workers-only PR preview (no ingest/Electric); see docs/pr-previews.md label Nov 3, 2026
Picking Page for the paths anchor made an anchor with no path yet, which
the URL leaves out; the navigation then rebuilt the drafts from the URL
and the anchor fell back to an event. A navigation now keeps the drafts
when it carries the definition they already produce.
@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

🍁 Maple PR preview

Note

Preview resources were removed when the preview label came off.

Final commit 9a46fb5 · View workflow run

@maple-review-bot

This comment has been minimized.

@maple-review-bot maple-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

1 inline note from Maple's review. The score and summary are in the review comment above.

Walking back from the anchor, a sequence that runs out is where it
started, not where it ended.
@maple-review-bot

This comment has been minimized.

Previews and local stacks seeded through /internal/demo/seed had traces,
logs and metrics but no session_replays or product_events, so Web
analytics showed its setup state and Product analytics had nothing to
show. The seed now adds 40 visitors an hour walking a signup funnel.
The row keeps the Paths tab's direction, so it opens paths from the
event, after or before.
session_replays and product_events map OrgId from the ingestion key
org_id, so the guard saw no org tag on their rows and refused the demo
seed's sessions. It now checks org_id like the other two tags: every tag
a row carries must be the writing org.
@maple-review-bot

This comment has been minimized.

@maple-review-bot maple-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

1 inline note from Maple's review. The score and summary are in the review comment above.

The kind filter and a fixed-width search field overflowed a 390px panel;
the row now wraps and the field takes the full width until sm.
Switching Drop-off to Bars kept the timing and leaver columns until the
new answer arrived, because they followed the rows on screen.
@maple-review-bot

This comment has been minimized.

@Makisuo

Makisuo commented Nov 3, 2026

Copy link
Copy Markdown
Collaborator Author

Traces from the preview test (deployment.environment = pr-2172, 10:40 to 11:02 UTC)

Span Spans p50 p95
QueryEngine.getProductEventNames 69 327 ms 1.23 s
QueryEngine.getProductEventsFunnelWidget 24 687 ms 3.32 s
QueryEngine.getProductEventsPathsWidget 14 490 ms 1.21 s

The funnel p95 comes from three requests in one burst (10:52:01 to :05; 5.48 s, 3.60 s, 1.74 s). The slowest trace, 8f2e84a00186b866ce461822c50dbd48, doesn't spend that time on the server:

FetchProductAnalyticsFunnel (maple-web)                 5.48s
  POST /_api/internal/query-engine/product-events-funnel  5.48s  (browser)
    POST /_api/* (maple-web-worker)                         134ms
      POST /internal/query-engine/product-events-funnel (maple-api)  112ms
        QueryEngineService.cachedDirect → EdgeCacheService   12ms
        AuditLogService.record                               83ms
LoadOnboardingChecklist (maple-web)                     5.48s  (server: 72ms)

The browser waited about 5.3 s before both requests in that navigation reached the worker, the funnel and the onboarding checklist alike. The same burst then ran under 0.8 s. That points at the test browser's connection queue, not the funnel query. The funnel's own queries (base, timing and leavers, the last two concurrent) are not visible here, because the edge cache answered. No errors on any of these spans.

Not from this PR, but noticed: AuditLogService.record takes 83 ms inside a cached query read.

@maple-review-bot maple-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

1 inline note from Maple's review. The score and summary are in the review comment above.

Back to an entry with the same steps but another window kept the pause,
which then wrote the half-typed step into that entry. The page now
remembers the URL it asked for; any other route change cancels the
pause, while its own write still leaves a newer pause running.
@maple-review-bot

This comment has been minimized.

@maple-review-bot maple-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

1 inline note from Maple's review. The score and summary are in the review comment above.

…lytics-page

# Conflicts:
#	apps/web/src/page/analytics/view-filters.ts
#	packages/query-engine/src/execution/executor.ts
Both were cut to six rows while Share divided by the full total. Each is
already bounded by Branches and Steps, so they now show them all.
The events read asks for one name past the limit, so a complete 200-name
answer no longer reads as capped. The paths tables sit in the kit grid.
@maple-review-bot

This comment has been minimized.

@maple-review-bot maple-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

2 inline notes from Maple's review. The score and summary are in the review comment above.

…e, names trimmed

- A Count by or view change no longer draws the previous counts under the
  new labels: a refetching funnel shows its loading state, as paths does.
- Back to another window mid-typing restarts the typing pause, so the
  typed step lands in the entry now showing instead of never running.
- Step and anchor names are trimmed before they reach the query.
- The Top custom event tile says when it only saw the top 200 names.
@maple-review-bot

This comment has been minimized.

@Makisuo Makisuo removed the preview:web Deploy a Workers-only PR preview (no ingest/Electric); see docs/pr-previews.md label Nov 3, 2026
@maple-review-bot

maple-review-bot Bot commented Nov 14, 2026 •

Copy link
Copy Markdown

Maple review

🟢 Confidence 10/10 · safe to merge. New page plus a stricter org-scope guard on ingest, the guard pinned by a foreign org_id test; the page's states are pinned by scene tests.
no findings · tests covered · risk high · 2/2 new units observable

Adds a /product-analytics page (events, funnels, paths) on the shared step builder and filter rail, a demo-seed generator for sessions and product events, and makes the ingest org guard read org_id. Safe to merge. The open McpQueryErrors traced to apps/web/src/page/registry.ts come from the agent's MCP registry; this diff only registers the new route there and does not fix them.

  • /product-analytics holds the URL-borne funnel and paths definition and debounces typed steps 450 ms
  • navigate writes the drafts into every navigation and keeps a blank anchor the URL omits
  • analyticsFilterRail is extracted to the kit, with Traffic and Event sections optional
  • The ingest org guard also refuses a row whose org_id names another org

Production impact

Open errors in the changed files (2)
Issue Service Occurrences File
@maple/mcp/errors/McpQueryError (7 issues) maple-ai 33 apps/web/src/page/registry.ts
@maple/mcp/errors/McpQueryError (3 issues) maple-chat 15 apps/web/src/page/registry.ts

After this merges, Maple checks whether they stop.

477ddce · What was checked, coverage and telemetry details are on the Maple / review check. Reply "won't fix" to dismiss a finding, or mention @maple-review-bot to ask about one.

This branch was previously deployed

1 inactive (outdated) deployment
pr-preview — 9a46fb5d Deployed Nov 3, 2026 by Makisuo via deploy-pr-preview #8926
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.

1 participant