feat(analytics): add route analytics heatmaps#1520
Conversation
Merge codex/route-heatmaps into the analytics overview filters branch, adding heatmap API routes, token signing, dashboard heatmap page, and dev-tool/event-tracker support.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
@greptile-ai review |
📝 WalkthroughWalkthroughThis PR introduces a complete analytics heatmap feature enabling click-level session replay visualization. It spans ClickHouse schema extensions for click aggregation, origin-bound JWT tokens for secure access, backend query and token endpoints, improved client-side click event tracking with viewport scaling and element chain serialization, a dashboard UI for token management and element preview, and a dev-tool overlay with interactive filtering and marker visualization. ChangesHeatmap Feature
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
7 issues found across 22 files
Tip: instead of fixing issues one by one fix them all with cubic
Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.
Re-trigger cubic
- showHeatmap: handle token-creation failure (reset dialog) + call via runAsynchronouslyWithAlert so errors surface (greptile P1, vercel, cubic) - DEVICE_WIDTH_BUCKETS: use Map to avoid prototype-pollution lookup (greptile P2) - dedup formatClickhouseDateTimeParam/parseBoundedDateTime across heatmap routes by exporting from the internal route (greptile P2) - derive heatmap JWT expiry from HEATMAP_TOKEN_TTL_MS (cubic) - only report 'Invalid route regex' for actual ClickHouse regexp errors, via shared isClickhouseRegexpError helper (cubic, both routes) - generic top-elements error text instead of raw err.message (cubic) - dev-tool: add primary-button :hover style to prevent hover flash (cubic) - selector builder: emit the data-test-id attr name actually matched (cubic)
|
@greptile-ai review |
There was a problem hiding this comment.
1 issue found across 6 files (changes from recent commits).
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
Button already wraps onClick in useAsyncCallback (loading/disabled tracking) and runAsynchronouslyWithAlert (error surfacing), so the explicit wrapper bypassed loading state. Pass the async handler directly per the established Button idiom (cubic).
|
@greptile-ai review |
|
@greptile-ai review |
…erlay integration - Consolidated heatmap query logic into shared utility functions for better maintainability. - Updated heatmap overlay token handling to use consistent storage keys across components. - Improved error handling for ClickHouse queries, ensuring proper status reporting. - Added comprehensive tests for new query utilities and heatmap overlay functionality. - Refactored related components to utilize the new utility functions, enhancing code clarity and reducing duplication.
There was a problem hiding this comment.
2 issues found across 15 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/stack-shared/src/utils/dom.tsx">
<violation number="1" location="packages/stack-shared/src/utils/dom.tsx:18">
P2: Fallback doesn't handle leading digits, which are invalid at the start of a CSS identifier. `cssEscapeIdent("3foo")` returns `"3foo"` in non-DOM environments, but `.3foo` is an invalid selector (parsed as a numeric literal). `CSS.escape` handles this by emitting a unicode escape (`\33 foo`).</violation>
</file>
<file name="apps/backend/src/lib/analytics-clickmap-query.ts">
<violation number="1" location="apps/backend/src/lib/analytics-clickmap-query.ts:379">
P2: Applying linear sampling scale-up (`scaleCount`) to `uniqExact` (unique users/replays) is statistically unsound. The sampling is event-level (hash includes timestamp), so unique entities are not sampled at the same rate as events — most users/replays survive even at low sampling rates. Only `count()` aggregates (clicks) should be scaled; unique counts should either be left unscaled or estimated with HyperLogLog/extrapolation methods designed for cardinality under sampling.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Fix all with cubic | Re-trigger cubic
| if (typeof CSS !== "undefined" && typeof CSS.escape === "function") { | ||
| return CSS.escape(value); | ||
| } | ||
| return value.replace(/[^a-zA-Z0-9_-]/g, (char) => `\\${char}`); |
There was a problem hiding this comment.
P2: Fallback doesn't handle leading digits, which are invalid at the start of a CSS identifier. cssEscapeIdent("3foo") returns "3foo" in non-DOM environments, but .3foo is an invalid selector (parsed as a numeric literal). CSS.escape handles this by emitting a unicode escape (\33 foo).
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/stack-shared/src/utils/dom.tsx, line 18:
<comment>Fallback doesn't handle leading digits, which are invalid at the start of a CSS identifier. `cssEscapeIdent("3foo")` returns `"3foo"` in non-DOM environments, but `.3foo` is an invalid selector (parsed as a numeric literal). `CSS.escape` handles this by emitting a unicode escape (`\33 foo`).</comment>
<file context>
@@ -5,3 +5,15 @@ export function hasClickableParent(element: HTMLElement): boolean {
+ if (typeof CSS !== "undefined" && typeof CSS.escape === "function") {
+ return CSS.escape(value);
+ }
+ return value.replace(/[^a-zA-Z0-9_-]/g, (char) => `\\${char}`);
+}
</file context>
| return value.replace(/[^a-zA-Z0-9_-]/g, (char) => `\\${char}`); | |
| let result = value.replace(/[^a-zA-Z0-9_-]/g, (char) => `\\${char}`); | |
| // CSS idents cannot start with a digit; use unicode escape | |
| if (/^\d/.test(value)) { | |
| result = `\\${value.charCodeAt(0).toString(16)} ` + result.slice(1); | |
| } else if (/^-\d/.test(value)) { | |
| result = `-\\${value.charCodeAt(1).toString(16)} ` + result.slice(2); | |
| } | |
| return result; |
|
|
||
| return { | ||
| samplingPct, | ||
| routes: routesRows.map((row) => ({ path: row.path, clicks: scaleCount(row.clicks), users: scaleCount(row.users), replays: scaleCount(row.replays) })), |
There was a problem hiding this comment.
P2: Applying linear sampling scale-up (scaleCount) to uniqExact (unique users/replays) is statistically unsound. The sampling is event-level (hash includes timestamp), so unique entities are not sampled at the same rate as events — most users/replays survive even at low sampling rates. Only count() aggregates (clicks) should be scaled; unique counts should either be left unscaled or estimated with HyperLogLog/extrapolation methods designed for cardinality under sampling.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/backend/src/lib/analytics-clickmap-query.ts, line 379:
<comment>Applying linear sampling scale-up (`scaleCount`) to `uniqExact` (unique users/replays) is statistically unsound. The sampling is event-level (hash includes timestamp), so unique entities are not sampled at the same rate as events — most users/replays survive even at low sampling rates. Only `count()` aggregates (clicks) should be scaled; unique counts should either be left unscaled or estimated with HyperLogLog/extrapolation methods designed for cardinality under sampling.</comment>
<file context>
@@ -0,0 +1,404 @@
+
+ return {
+ samplingPct,
+ routes: routesRows.map((row) => ({ path: row.path, clicks: scaleCount(row.clicks), users: scaleCount(row.users), replays: scaleCount(row.replays) })),
+ selectors: selectorsRows.map((row) => ({ selector: row.selector, clicks: scaleCount(row.clicks) })),
+ elements: elementsRows.map((row) => ({
</file context>
| routes: routesRows.map((row) => ({ path: row.path, clicks: scaleCount(row.clicks), users: scaleCount(row.users), replays: scaleCount(row.replays) })), | |
| routes: routesRows.map((row) => ({ path: row.path, clicks: scaleCount(row.clicks), users: Number(row.users), replays: Number(row.replays) })), |
…tency - Updated all instances of "heatmap" to "clickmap" in the analytics module to reflect the new terminology. - Adjusted console messages, alerts, and UI elements to align with the clickmap branding. - Modified navigation items and dev-tool components to ensure cohesive user experience across the application.
- Added new API routes for clickmap data retrieval, including public and internal endpoints. - Implemented token generation and validation for clickmap access, ensuring secure data handling. - Refactored existing heatmap references to clickmap throughout the codebase for consistency. - Enhanced error handling and validation for clickmap queries, improving robustness. - Updated related components and tests to support the new clickmap features and ensure functionality.
There was a problem hiding this comment.
1 issue found across 29 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/template/src/dev-tool/dev-tool-styles.ts">
<violation number="1" location="packages/template/src/dev-tool/dev-tool-styles.ts:2731">
P2: CSS specificity bug: `.sdt-hm-mode-btn:hover` (specificity 0,3,0) overrides `.sdt-hm-mode-btn-active` (specificity 0,2,0) when hovering the active button. This makes the white text revert to `var(--sdt-text)` on the accent background, causing a visual glitch.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Fix all with cubic | Re-trigger cubic
| cursor: pointer; | ||
| } | ||
|
|
||
| .stack-devtool .sdt-hm-mode-btn:hover { |
There was a problem hiding this comment.
P2: CSS specificity bug: .sdt-hm-mode-btn:hover (specificity 0,3,0) overrides .sdt-hm-mode-btn-active (specificity 0,2,0) when hovering the active button. This makes the white text revert to var(--sdt-text) on the accent background, causing a visual glitch.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/template/src/dev-tool/dev-tool-styles.ts, line 2731:
<comment>CSS specificity bug: `.sdt-hm-mode-btn:hover` (specificity 0,3,0) overrides `.sdt-hm-mode-btn-active` (specificity 0,2,0) when hovering the active button. This makes the white text revert to `var(--sdt-text)` on the accent background, causing a visual glitch.</comment>
<file context>
@@ -2640,39 +2656,209 @@ export const devToolCSS = `
+ cursor: pointer;
+ }
+
+ .stack-devtool .sdt-hm-mode-btn:hover {
+ color: var(--sdt-text);
+ }
</file context>
| .stack-devtool .sdt-hm-mode-btn:hover { | |
| .stack-devtool .sdt-hm-mode-btn:not(.sdt-hm-mode-btn-active):hover { |
- Introduced auto-copy feature for clickmap token snippets, improving user experience. - Updated CopyField and CopyButton components to support initial copied state. - Refactored PageClient to manage custom origin state and improve clickmap token handling. - Added viewport warning styles and functionality to enhance user guidance in the dev tool. - Cleaned up unused code and improved overall component structure for better maintainability.
Summary
Adds route analytics heatmaps, stacked on top of
codex/analytics-overview-filters(#1496)./analytics/heatmap, internal heatmap + heatmap-token endpoints)Notes
Base branch is
codex/analytics-overview-filtersso the diff shows only the heatmap changes. Will retarget todevonce the base PR lands.🤖 Generated with Claude Code
Summary by cubic
Adds analytics clickmaps (hour‑of‑week activity and session replay clickmaps) with a new Clickmaps dashboard, public/internal APIs, signed overlay tokens, shared ClickHouse query utilities, and tighter overlay/dev‑tool integration. Also renames “heatmap” to “clickmap” across the app, improves token verification and error handling, unifies overlay token handoff, adds camelCase SDK options, and polishes the overlay UX with auto‑copy tokens and viewport guidance.
New Features
@stackframe/stack-shared; admin app exposes getAnalyticsClickmap/createAnalyticsClickmapToken; SDK method supports camelCase options and maps to snake_case.Migration
Written for commit c20f4c2. Summary will update on new commits.
Summary by CodeRabbit
New Features
Style