Skip to content

[Release] Funnels, Journeys, Behavior, Search and CDN rebuilds + filter builder + settings polish - #126

Merged
uz1mani merged 83 commits into
mainfrom
staging
Jul 4, 2026
Merged

uz1mani merged 83 commits into
mainfrom
staging

Conversation

@uz1mani

@uz1mani uz1mani commented Jul 4, 2026 •

Copy link
Copy Markdown
Member

Summary

  • Rebuilds five analytics surfaces to the sharp/flat design system: Funnels (real detail pages), Journeys, Behavior, Search and CDN — plus the dashboard filter builder that set the pattern.
  • Kills the fake-empty/flicker bug class app-wide on these pages: keepPreviousData everywhere, first-load-only skeletons, explicit error cards with retry, settled-only empty states, URL-synced state.
  • Carries the June settings/admin/billing polish wave (sticky settings nav, humanized audit log, plan-name single source of truth, favicon privacy proxy, usage progress bar, marketing copy fixes).

Changes

Behavior, Search & CDN facet treatment (05-07, 9 commits — 9c71d883..f79ef47a)

  • Behavior: KPI band with guarded inverted deltas, URL-synced ?page= lens filtering the rage/dead element tables, real daily trend chart (new backend endpoint), scroll-depth rails replacing the radar chart (drops recharts from the route), view-all dialogs on the shared dialog primitive.
  • Search: promoted to the full GSC surface — Queries / Pages / Countries / Devices / Opportunities views, dual-scale traffic chart (clicks left / impressions right), per-query position sparklines in row expansions (race-free per-row SWR), sync-status line, new-queries popover, pagination + view + expansion in the URL.
  • CDN: bandwidth and requests split into cached vs origin with per-day hit rate, origin-latency chart, error charts recolored (3xx neutral, 4xx/5xx red), ranked datacenter rows with region-scoped country mapping (fixes Amsterdam/Frankfurt flagged as CA/US; UK→GB), dead requests column removed.
  • Shared: useUrlDateRange (renamed from useFunnelDateRange) on all date-ranged pages; keepPreviousData on all 12 GSC/Bunny hooks; useSite replaces full-dashboard fetches for document titles.

Funnels & Journeys rebuild (04-07, 24 commits — 8be2614f..ed1cd250)

  • Journeys: single-row h-10 toolbar (steppers + entry combobox + segmented view switch), shareable ?lens= selection across both views, ambient transition ribbons, declarative React-SVG flow view, country/device/referrer dimension filters, zero-flicker loading.
  • Funnels: real [funnelId] detail route (KPI band, funnel canvas with connecting bands, step drill-down with exits + breakdowns, conversion trend chart, dashboard filters), summary-row list with real step targets and sparklines, modal rebuilt on the dialog primitive with suggestions and a conversion-window control (kills the hardcoded 30d overwrite bug).
  • Shared Select component now portals its dropdown (fixes clipping in scrollable containers, 18 usages).

Dashboard filter builder (04-07, 11 commits — a098431f..fbf3454e)

  • Single-surface anchored popover replaces the filter modal: dimension stage with keyboard nav, value stage with operator chip, iconography, suggestion-failure surfacing, no loading flicker.

Settings / admin / billing / marketing polish (June, ~35 commits)

  • Sticky settings header + nav, humanized audit-log entries, member names instead of raw UUIDs, one plan-name source of truth, encrypted-profile notice, bot-spam load failures surfaced, favicon proxy stops leaking visited domains to Google (+ CDN-cacheable), usage progress bar on billing, /switch preselects the current interval, tiny-base KPI delta guard, admin/setup chrome recolored to brand orange, compare-table sentiment colors, sw.js excluded from the auth matcher, facet 0.2.2 + support-widget 0.10.0.

Test Plan

  • Full gate green at f79ef47a: tsc 0 errors · build exit 0 · 226 vitest passing
  • Staging Playwright passes: funnels/journeys (24 + 43 + 13 checks, 04-07) · behavior/search/CDN (batches A/B/C: seeded frustration data, real GSC + Bunny data on the ciphera.net site, 40+ checks incl. zero-flicker frame-sampling, error paths, deep links, mobile 390)
  • Geometry/label census clean on all touched files (0 glass, 0 non-sanctioned rounding, mono label rule)
  • After production deploy: spot-check /behavior, /search, /cdn on a connected production site (GSC + Bunny states, dual-scale chart, datacenter flags)
  • Merge ciphera-net/pulse-backend staging → main alongside this PR (the behavior trend chart reads the new /frustration/daily endpoint; journeys filters read the extended transitions endpoints)

uz1mani added 30 commits June 25, 2026 09:11
The service worker script was matched by the auth middleware and
307-redirected to /login for visitors without a session, so every page
logged "SecurityError: ServiceWorker script is behind a redirect" and
PWA/offline never activated. Static service-worker assets now bypass
the matcher like manifest.json already does.

Present on production too — promoting staging picks up the fix there.
Features page claimed 10,000 pageviews/month; the pricing page and plan
data say 5k. Align the copy with the real plan limit.
"Cookie Banner Required: false" (Pulse's headline win) rendered as a
red ✗ while GA's "true" got a green ✓ — the first row of the table
visually awarded GA the win. Cells where the raw boolean inverts
sentiment now use worded values with an explicit good/bad tone
("None" green / "Required" red).
The Popular and category-group headers rendered sentence-length
UPPERCASE via CSS (e.g. "STATIC SITES & DOCUMENTATION"), which the
label rule reserves for 1-2-word data labels. Drop the uppercase
transform and wide tracking; the h2s keep their display font.
TAB_META in useHomePageMeta covered every settings tab except roles, so
/settings/organization/roles fell back to "Settings" and the topbar
read "Settings › Settings".
Zero importers across all import forms (static, dynamic, require);
the marketing funnel mockup uses its own local copy. 2,042 LOC.
Favicons in the authenticated Referrers/Pages panels (and the marketing
demo + share-page OG image) loaded straight from Google's s2 service,
sending every customer's referrer and page domains plus the user's IP
to Google. All consumers now go through a same-origin /api/favicon
proxy: the upstream fetch happens server-side behind the CDN cache, so
browsers only ever talk to Pulse. CSP drops the google/gstatic
allowances so any regression fails visibly instead of silently.
…-empty

The sessions request error was swallowed, so a 500 rendered as
"No suspicious sessions found" — a failed request looked like a healthy
site. The list now has explicit error (with retry) and loading states;
the empty state only renders for a genuinely empty successful response.
…vailable

Display Name and Email rendered silently empty whenever the vault PII
was never provisioned (a sign-in through Pulse's own form never sets
the ciphera_pii cookie that Ciphera ID drops on .ciphera.net — same
behaviour on prod). The tab now explains why the fields are empty and
points at Ciphera ID as the recovery path, instead of pretending the
profile has no name and no email.
Local overlays (dialog, tooltip, select dropdown, date-range picker)
shipped border-only and read flat against the dark canvas, while
Facet's own Modal/Tooltip cast shadow-lg/xl — inconsistent depth within
one product. Dialogs get shadow-xl, smaller floats shadow-lg; the
command palette composes DialogContent so it inherits the fix.
…rolling

Long tabs (Privacy, Notifications) scrolled both away, losing context
and the tab switcher. The header sticks to the top of the scroll
container; the nav sticks below it and scrolls internally when taller
than the viewport.
The page rendered all 18 release sections at once (~29k px desktop,
~53k px mobile) and showed two "Changelog" H1s (page hero + the
markdown's own). The five most recent sections render expanded, the
rest sit behind a native details disclosure, the markdown H1 is
stripped, and heading dates display as DD/MM/YYYY like the rest of the
product. Keep-a-changelog link definitions are appended to both chunks
so reference links keep resolving.
The default "All" view rendered every integration as a full card
(~8.6k px desktop / ~21k px mobile); it now uses compact icon+name
tiles and switches to full cards when a category or search narrows the
list. Black-only brand logos were styled fill-current invert — the
inherited text colour is already light, so inverting it painted the
glyphs near-black on the dark tiles (Vercel, GitHub Pages, Eleventy and
17 more); they are now plain white fills, WooCommerce joins them, and
Solid.js uses its light-blue brand tone.
…g copy

The Privacy tab's ad-hoc ternary recognised only 'pro' and labelled
every other plan "Free" — a Pioneer org read "Your Free plan supports
up to 2 years". Billing already had the correct mapping; it now lives
in lib/utils/plan.ts and both tabs consume it.
Zero-PII members with no stored email showed their 36-char user id as
the display name. The signed-in member reads "You", pending invites
show their invite email, and the rest fall back to a short member id.
Unmapped actions leaked raw snake_case event names and payloads
rendered as a JSON blob. Unknown actions fall back to a sentence-cased
form and payload entries render as labelled key/value fields.
Visiting /auth/callback without a code param (stale link, prefetch,
direct visit) spun the loading overlay forever. It now shows the
existing error card with a way back to login.
The install step promised a snippet but rendered nothing when the
setup session had no site. It now says why there's no snippet, offers
a way back to the site step, and keeps an explicit continue.
The admin grant-plan quick links (+1 Month / +1 Year / Forever) were
blue and the setup done-page tiles used blue/purple/amber — UI chrome,
not data-viz, so they follow the system accent. Chart series and
telemetry chips keep blue by design.
Five DD/MM ticks collide at 390px (real dashboard and the marketing
demo). numTicks is now an upper bound; narrow charts budget ~80px per
label and drop to what fits.
…lain engagement percentiles

A previous period with fewer than 10 visitors turns every delta into
double-digit noise and paints the whole KPI strip red; those windows now
show no delta. The cryptic engagement sub-score row (S P77 · T P88 …)
gets a plain-language hover explanation of the percentile ranks.
A yearly org landed on a Monthly-defaulted toggle, misquoting their own
plan. Ref-guarded so revalidation doesn't stomp a manual toggle.
693 / 100,000 takes arithmetic to read as 0.7%; the bar makes headroom
visible at a glance and turns red at 90%.
Every other tool tab keeps its h1 in the empty state; these two dropped
straight into a centered connect card with no page identity.
…n helpers

Also routes the sites/new limit toast through it — it interpolated the
raw plan_id ("free plan limit reached").
…ls, site cards

- Installation referenced "Settings → Goals & Events"; the tab is Goals.
- Invalid-invite page sent anonymous visitors to "Go to dashboard"
  (which is a login wall) — now "Go to Pulse".
- Funnels showed two identical orange create CTAs when empty and mixed
  "Create Funnel"/"Create funnel" casing.
- All-zero 24h site-card stats render as em dashes instead of a wall of
  0s that reads like broken tracking.
Two transitions inlined the tuple literally; drift-prone copies of a
constant that already exists in lib/motion.
At the min position there is no orange range beside the thumb and the
background-coloured square vanished into the canvas (pricing slider read
as a sliver). Solid fill plus a ::before overlay that extends the touch
area past the visual 20px.
… affordance

Empty table cells mixed "-" and blank; the quarantine active-tab
underline was white where the app convention is orange; the admin nav
cards gained a border hover state so they read as clickable.
uz1mani added 28 commits July 4, 2026 16:27
The list's date range lifts into ?period/start/end via useFunnelDateRange
(the period URL grammar extracts to lib/hooks/periodUrl.ts, now shared
with journeys, including a clamp-to-today shift helper — tested). The
accordion cards become summary rows: the whole card links to the detail
route carrying the current range, edit/delete float as hover siblings
(no more nested buttons), step chips show the real page/event targets
with kind glyphs, the conversion % is plain white with a tiny-base-
guarded delta off a local-date-parts previous range (the toISOString
UTC drift dies), and a lazy orange sparkline rides the right rail. A
failed list fetch now shows an error card with Retry instead of the
fake 'No funnels yet', and the skeleton mirrors the real layout.
funnels/[funnelId] stops redirecting and becomes the real route: quiet
back link, name + description + mono window/created meta, URL-synced
DateRangePicker, Edit (modal) and Delete (confirm → back to list), and
a five-KPI band — Conversion / Visitors / Converted / Biggest drop-off
(showing the step's real value, never the default name, and no more
green 0%) / Median time — with tiny-base-guarded deltas off the shared
previousDateRange helper (local date parts; the summary card now uses
it too). 404s get a friendly not-found card, fetch failures an error
card with Retry, range changes the Updating chip. The edit route
redirects to the detail, the breadcrumb renders the funnel's name under
a linked Funnels parent, and the orphaned detail skeleton is rebuilt to
the real layout.
Equal-width step columns over vertical level rails filled bottom-up to
each step's conversion-of-start (orange fill, 2px solid top edge), with
SVG trapezoid bands measured off the real rail rects connecting adjacent
levels and red −N · x% drop labels in the gutters. Clicking or arrowing
across columns writes ?step= (default stays out of the URL); the
selected column carries an orange ring. Zero-visitor periods show flat
rails with an inline note, single-step funnels get one column and no
bands, and below md the columns stack with short vertical connectors —
the emerald bars are gone and the product funnel finally looks like the
one the homepage sells.
A two-pane strip under the canvas follows the selected step: exits on
the left (kind glyphs, proportional bars, 'Entry step' note on step 1)
and a dimension breakdown on the right — the Select offers exactly the
keys the backend's ValidDimension accepts, rows carry the dashboard's
flag/brand icons with visitors and conversion %. getFunnelBreakdown
gains an SWR hook (cached per step+dimension, stale kept while
revalidating), panes height-morph on step change, spinners obey the
150ms rule in stable-height boxes, and empties only render settled.
Overall conversion over time as the single orange series on the shared
area-chart primitives (they compose cleanly for multi-series — the
minimal-SVG fallback wasn't needed). Per-step series toggle on via quiet
chips (orange text only when active, showing real step targets) and draw
in neutral-400 so the color budget holds. Numeric DD/MM x-axis, % y-axis,
tooltip through the chart system, stable-height card with the Updating
chip on range changes and an error card with Retry.
The dashboard filter system lands on the detail page wholesale — pills,
button, single-surface popover, duplicate guard and edit-via-pill all
identical — with committed filters serialized into ?filters= by the
dashboard's exact codec and fed to stats (current and previous period),
trends and breakdowns. The per-dimension suggestion fetcher extracts
from the dashboard page into lib/hooks/useFilterSuggestions (the
dashboard now consumes the shared hook; behavior unchanged) so every
filtered surface resolves values the same way.
The hand-rolled overlay becomes a real dialog (ui/dialog.tsx — focus
trap, focus return, Esc, aria, sanctioned shadow), max-w-2xl with
content-fit height and sentence-case copy. The step editor gets square
number chips, Page|Event as a segmented control, the app Select for
matchers and property operators, 32px reorder targets and height-morph
on add/remove. Path and event inputs gain anchored suggestion panels
fed once per open from getDashboardPages/getDashboardGoals (kind
glyphs, counts, arrow keys, free text always allowed, Escape closes
the panel without killing the dialog, 150ms spinner rule). The
conversion-window control (number + unit + 24h/72h/7d/14d/30d presets,
create default 7d, edit prefilled) submits exactly what it shows —
the hardcoded 30-day overwrite that silently rewrote every edited
funnel's window is dead. Validation is inline with field errors and
focus-to-field; toasts only fire on submit failures. funnels/new
forwards ?prefill= and the list page auto-opens a seeded create modal.
The journeys lens chip gains a one-click 'Create funnel from this path'
— spineThrough (tested) walks the heaviest hop in both directions from
the lens (skipping (other), capped at six steps) and lands on the
funnels page with the create modal prefilled via ?prefill=. In the
other direction, the funnel step strip links exact page steps to
journeys entered from that page.
Six Unreleased → Improved entries: the real funnel detail page, the
window-overwrite fix, step suggestions, the one-row journeys toolbar,
the shareable lens with cross-links, and the zero-flicker loading
discipline.
Radix's escape listener runs on document capture, so it dismissed the
whole dialog before the panel's bubble-phase handler could claim the
key. An open-panel counter now feeds DialogContent's onEscapeKeyDown:
first Escape closes the panel, the next one closes the dialog.
…tion input

Blur schedules a 120ms delayed close so row clicks land before the
panel unmounts — but refocusing the same input inside that window let
the stale timer close the freshly opened panel. Focus now clears it.
The backend filter param threads through lib/api/journeys + the SWR
hooks, and useJourneyFilters gains the dashboard ?filters= codec
(parsed/serialized identically, cleared by Reset, counted in isDefault).
The toolbar gains the dashboard FilterButton and a pills row, driving
the same single-surface popover — DimensionStage takes an additive
'allowed' allowlist so journeys offers only Country/Device/Referrer
while the dashboard is untouched. Suggestions reuse the shared
useFilterSuggestions hook, and a distinct filtered-empty state covers
the no-match case. Combined lens+filter state is fully URL-shareable.
The step editor was a jagged stack of bordered cards — the Page/Event
toggle and the matcher select sat on different rows at different widths,
so the name and path inputs never lined up, and a low-value 'Step name'
field (never in the approved design) padded every step. Steps are now
single aligned rows — number chip, Page|Event, matcher, value input,
then reorder and delete — with property filters indented beneath event
steps. The step label auto-derives from its target (custom names on
older funnels are preserved on edit). The conversion-window number
input drops from full-width to w-20 and sits on one row with the unit
select and preset chips behind a divider.
inputClass leads with w-full, which beats a plain appended w-20
(Tailwind resolves same-property utilities by stylesheet order), so the
window number input rendered full-width and pushed the unit and presets
to a second row. !w-20 forces the compact width; the input, unit select
and preset chips now sit on one line.
…ndard

The input-variant Select had three latent problems that broke it inside
dialogs: min-w-[140px] overflowed narrow slots (overlapping siblings),
h-11 didn't match the app's h-10 controls, and the absolute-positioned
dropdown was clipped by any scrollable ancestor (so it vanished inside
the funnel modal). The dropdown now portals to the body with fixed
positioning (measured from the trigger, flips up near the viewport
bottom, repositions on scroll/resize), the input variant is h-10 with
min-w-0 and the ring-2 brand-orange focus the rest of the controls use,
and options/labels truncate. Applies to all 18 Select usages.
Every control is now h-10, border-border, rounded-none, with the same
ring-2 brand-orange focus — no more mixed h-11 selects, h-8 chips, and
three different focus rings. The window number input loses its native
browser spinner, the matcher/operator selects are sized to fit (no
overlap now that Select respects its slot), preset chips are h-10 to
match the row, and the suggestion panels carry the sanctioned shadow.
… on screen

Hovering a row highlighted every occurrence of its path across all
steps, because it seeded the chain from chainThrough(path) (all
occurrences). Hover now seeds from the specific step:path node
(chainThroughNode), so hovering /relay at step 2 lights up only its own
flow; the pinned lens keeps its deliberate trace-the-page-everywhere
behaviour. The flow tooltip flips to the left of the cursor in the right
portion of the viewport so it no longer runs off the last column, and
its text truncates.
The columns view stays readable at ten rows per step regardless of the
Paths density; the flow view remains the detailed lens for higher
counts.
The conversion trend is now just the overall orange series — the
per-step multi-series toggles were janky and the step drill-down already
lives in the strip above.
@uz1mani uz1mani self-assigned this Jul 4, 2026
@uz1mani
uz1mani merged commit 8d777fe into main Jul 4, 2026
3 checks passed
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