Skip to content

Add table filter integration: types, getFilter mappings, DataTable on…#86

Merged
izadoesdev merged 6 commits into
databuddy-analytics:mainfrom
alex-holovach:i239hi55h9pzim9jfds6h-6532622b/feature/add-table-filtering-website-overview-2025-08-15
Aug 15, 2025
Merged

Add table filter integration: types, getFilter mappings, DataTable on…#86
izadoesdev merged 6 commits into
databuddy-analytics:mainfrom
alex-holovach:i239hi55h9pzim9jfds6h-6532622b/feature/add-table-filtering-website-overview-2025-08-15

Conversation

@alex-holovach

@alex-holovach alex-holovach commented Aug 15, 2025

Copy link
Copy Markdown
Contributor

…AddFilter wiring

Pull Request

Description

Please include a summary of the change and which issue is fixed. Also include relevant motivation and context.

Checklist

  • My code follows the style guidelines of this project
  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes

Summary by CodeRabbit

  • New Features

    • Click analytics table rows to add filters (inline and full‑screen); per‑tab filtering for Devices, Browsers, Operating Systems, Pages, Referrers, and UTM fields.
    • Analytics toolbar added to website pages: date range picker, quick ranges, daily/hourly toggle, refresh button with toasts, and filter chips with add/remove.
    • Custom Events table shows property‑level details and supports row‑based filtering.
    • Sessions list: paginated/infinite scroll, improved loading states, expandable session rows.
  • Style

    • Minor layout and spacing refinements.

@vercel

vercel Bot commented Aug 15, 2025

Copy link
Copy Markdown

@github-actions[bot] is attempting to deploy a commit to the Databuddy Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Aug 15, 2025

Copy link
Copy Markdown
Contributor

Caution

Review failed

The pull request is closed.

Walkthrough

Adds end-to-end row-to-filter plumbing and UI: exposes addFilter on tab props, adds onAddFilter/getFilter to DataTable and table tabs, introduces dynamic filter atoms and toolbar/layout for managing filters and refresh, updates session atoms and list to atom-driven pagination, and adjusts query builders and geo normalization.

Changes

Cohort / File(s) Summary
DataTable API & Tab types
apps/dashboard/components/analytics/data-table.tsx, apps/dashboard/lib/table-tabs.tsx, apps/dashboard/app/(main)/websites/[id]/_components/utils/types.ts
Adds onAddFilter to DataTableProps, getFilter to TabConfig/SimpleTabConfig, and addFilter to FullTabProps; wires getFilter through useTableTabs.
Overview tab & page wiring
apps/dashboard/app/(main)/websites/[id]/_components/tabs/overview-tab.tsx, apps/dashboard/app/(main)/websites/[id]/page.tsx
Wires addFilter into WebsiteOverviewTab and passes onAddFilter to multiple DataTable instances; adds per-tab getFilter handlers and switches page to atom-driven dynamic filters.
Toolbar & Layout (client UI)
apps/dashboard/app/(main)/websites/[id]/_components/analytics-toolbar.tsx, apps/dashboard/app/(main)/websites/[id]/layout.tsx
Adds AnalyticsToolbar and WebsiteLayout client components; ties UI to dynamicQueryFiltersAtom and isAnalyticsRefreshingAtom, handles refresh (query invalidation) and filter management.
Shared filter mappings & options
packages/shared/src/lists/filters.ts
Adds tableFilterMapping, deviceDisplayToFilterMap, and new filterOptions entries (referrer, path) to map table titles and device labels to filter fields/values.
Sessions: atoms, list, and exports
apps/dashboard/stores/jotai/sessionAtoms.ts, apps/dashboard/app/(main)/websites/[id]/sessions/_components/sessions-list.tsx, apps/dashboard/app/(main)/websites/[id]/sessions/page.tsx, apps/dashboard/app/(main)/websites/[id]/sessions/_components/index.ts, apps/dashboard/app/(main)/websites/[id]/sessions/_components/session-row.tsx, apps/dashboard/app/(main)/websites/[id]/sessions/_components/session-event-timeline.tsx
Adds session-related jotai atoms (expanded id, per-site page, auto-reset), rewrites SessionsList to atom-driven paginated fetch with event transformation and infinite scroll, tightens event/property types, updates country flag lookup, and removes old re-exports.
Filter atoms & hooks
apps/dashboard/stores/jotai/filterAtoms.ts, apps/dashboard/hooks/use-dynamic-query.ts
Adds dynamicQueryFiltersAtom, add/remove/clear atoms, tightens own-property check during serialization, and removes an obsolete comment in useDynamicQuery.
API query builders & utils
apps/api/src/query/builders/sessions.ts, apps/api/src/query/utils.ts, apps/api/src/query/simple-builder.ts, apps/api/src/query/builders/pages.ts, apps/api/src/query/builders/traffic.ts, apps/api/src/query/types.ts
Moves combined WHERE from inner CTE to outer query in sessions builder and enables geo normalization plugin; applyGeoNormalization falls back to row.country when name missing; simple-builder special-cases normalization for path and referrer; many query builders add allowedFilters and some gain customizable.
Types & shared models
packages/shared/src/types/analytics.ts
Adds country_code to BaseSessionData and ProfileData; narrows properties to Record<string, unknown> for profile sessions.
Minor UI/hygiene
apps/dashboard/app/(main)/websites/page.tsx
Removes an unused import and two ARIA attributes from the websites grid container.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant DataTable
  participant TabConfig
  participant WebsitePage
  participant FiltersAtom

  User->>DataTable: Click row
  alt row has sub-rows
    DataTable->>DataTable: Toggle expand
  else onAddFilter provided
    DataTable->>TabConfig: getFilter(row)
    TabConfig-->>DataTable: { field, value }
    DataTable->>WebsitePage: onAddFilter(field, value, tableTitle)
    WebsitePage->>FiltersAtom: addFilter({ field, operator: "eq", value })
  else fallback
    DataTable->>DataTable: onRowClick fallback
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~40 minutes

Possibly related PRs

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between f780616 and 572eef2.

📒 Files selected for processing (5)
  • apps/api/src/query/builders/pages.ts (4 hunks)
  • apps/api/src/query/builders/traffic.ts (4 hunks)
  • apps/api/src/query/simple-builder.ts (1 hunks)
  • apps/dashboard/app/(main)/websites/[id]/page.tsx (6 hunks)
  • apps/dashboard/stores/jotai/filterAtoms.ts (3 hunks)
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@alex-holovach

Copy link
Copy Markdown
Contributor Author

thanks @kubiks-agent for cooking!

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 11

🔭 Outside diff range comments (4)
apps/dashboard/components/analytics/data-table.tsx (2)

173-194: Update FullScreenTable onAddFilter prop typing to match DataTableProps

Ensure parity between the inline and full-screen flows.

- onAddFilter,
+ onAddFilter,
 ...
- onAddFilter?: (field: string, value: string, tableTitle?: string) => void;
+ onAddFilter?: (field: string, value: string | number, tableTitle?: string) => void;

1006-1016: Cursor affordance may mislead when onAddFilter is provided but no getFilter/fallback exists

The row shows cursor-pointer whenever onAddFilter is present, even if neither a getFilter is defined nor a reasonable fallback is available. This is minor UI friction.

Consider tightening the condition to only show pointer when:

  • hasSubRows, or
  • onRowClick, or
  • onAddFilter and (active tab has getFilter OR row.original.name exists)
apps/dashboard/app/(main)/websites/[id]/_components/tabs/overview-tab.tsx (2)

411-418: Critical: dayjs.tz()/dayjs.utc() used before plugins are extended (runtime bug).

filterFutureEvents calls dayjs().tz(...) and dayjs.utc(...) (Lines 413, 416) but dayjs.extend(utc) and dayjs.extend(timezone) are only called much later (Lines 780-781). This will cause tz/utc to be undefined at call-time and can throw.

Fix by extending the plugins right after importing them, before any use, and remove the late extensions.

Apply the following diffs:

  • Move dayjs.extend calls near imports:
 import dayjs from 'dayjs';
 import timezone from 'dayjs/plugin/timezone';
 import utc from 'dayjs/plugin/utc';
+dayjs.extend(utc);
+dayjs.extend(timezone);
  • Remove late extensions:
-	dayjs.extend(utc);
-	dayjs.extend(timezone);

Also applies to: 780-783


1183-1200: Fix: percentage is undefined in sub-row; avoid array index in keys.

  • valueItem.percentage is not set in processedCustomEventsData, so the UI renders undefined%.
  • Keys use the array index; guidelines recommend not using indexes.

Apply this diff to compute percentage on the fly and remove index usage from keys:

-									{propertyValues.map(
-										(valueItem: PropertyValue, valueIndex: number) => (
+									{propertyValues.map(
+										(valueItem: PropertyValue) => (
 											<div
 												className="flex items-center justify-between border-border/10 border-b px-3 py-2 last:border-b-0 hover:bg-muted/20"
-												key={`${propertyKey}-${valueItem.value}-${valueIndex}`}
+												key={`${propertyKey}-${valueItem.value}`}
 											>
 												<span
 													className="truncate font-mono text-foreground text-sm"
 													title={valueItem.value}
 												>
 													{valueItem.value}
 												</span>
 												<div className="flex items-center gap-2">
 													<span className="font-medium text-foreground text-sm">
 														{formatNumber(valueItem.count)}
 													</span>
 													<div className="min-w-[2.5rem] rounded bg-muted px-2 py-0.5 text-center font-medium text-muted-foreground text-xs">
-														{valueItem.percentage}%
+														{Math.round((valueItem.count * 100) / Math.max(1, propertyTotal))}%
 													</div>
 												</div>
 											</div>
 										)
 									)}

Optional follow-up: Populate percentage and percentage_within_event in processedCustomEventsData to avoid recomputing in render and to align with PropertyValue type.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 79adc87 and 2db0697.

📒 Files selected for processing (6)
  • apps/dashboard/app/(main)/websites/[id]/_components/tabs/overview-tab.tsx (12 hunks)
  • apps/dashboard/app/(main)/websites/[id]/_components/utils/types.ts (1 hunks)
  • apps/dashboard/app/(main)/websites/[id]/page.tsx (2 hunks)
  • apps/dashboard/components/analytics/data-table.tsx (9 hunks)
  • apps/dashboard/lib/table-tabs.tsx (3 hunks)
  • packages/shared/src/lists/filters.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (9)
**/*.{js,ts,jsx,tsx}

📄 CodeRabbit Inference Engine (.cursor/rules/ultracite.mdc)

**/*.{js,ts,jsx,tsx}: Don't import next/document outside of pages/_document.jsx in Next.js projects.
Don't use global eval().
Don't use var.
Don't use console.
Don't use debugger.
Don't assign directly to document.cookie.
Don't use duplicate case labels.
Don't use duplicate class members.
Don't use duplicate function parameter names.
Don't use empty block statements and static blocks.
Don't use with statements in non-strict contexts.
Don't use the arguments object.
Don't use the comma operator.
Don't use unnecessary boolean casts.
Use for...of statements instead of Array.forEach.
Use arrow functions instead of function expressions.
Use Date.now() to get milliseconds since the Unix Epoch.
Use .flatMap() instead of map().flat() when possible.
Use literal property access instead of computed property access.
Don't use parseInt() or Number.parseInt() when binary, octal, or hexadecimal literals work.
Use concise optional chaining instead of chained logical expressions.
Use regular expression literals instead of the RegExp constructor when possible.
Don't use number literal object member names that aren't base 10 or use underscore separators.
Remove redundant terms from logical expressions.
Use while loops instead of for loops when you don't need initializer and update expressions.
Don't reassign const variables.
Don't use constant expressions in conditions.
Don't use Math.min and Math.max to clamp values when the result is constant.
Don't return a value from a constructor.
Don't use empty character classes in regular expression literals.
Don't use empty destructuring patterns.
Don't call global object properties as functions.
Don't declare functions and vars that are accessible outside their block.
Don't use variables and function parameters before they're declared.
Don't use 8 and 9 escape sequences in string literals.
Don't use literal numbers that lose precision.
Don't use duplicate conditions in if-else-if chains.
Don't use two keys with the same name inside objects...

Files:

  • apps/dashboard/app/(main)/websites/[id]/_components/utils/types.ts
  • packages/shared/src/lists/filters.ts
  • apps/dashboard/lib/table-tabs.tsx
  • apps/dashboard/app/(main)/websites/[id]/page.tsx
  • apps/dashboard/app/(main)/websites/[id]/_components/tabs/overview-tab.tsx
  • apps/dashboard/components/analytics/data-table.tsx
**/*.{ts,tsx}

📄 CodeRabbit Inference Engine (.cursor/rules/ultracite.mdc)

**/*.{ts,tsx}: Don't use TypeScript enums.
Don't use TypeScript const enum.
Don't use TypeScript namespaces.
Don't use the TypeScript directive @ts-ignore.
Don't use any type.
Don't use implicit any type on variable declarations.
Don't use non-null assertions with the ! postfix operator.
Don't misuse the non-null assertion operator (!) in TypeScript files.
Don't declare empty interfaces.
Use export type for types.
Use import type for types.
Don't use primitive type aliases or misleading types.
Don't use empty type parameters in type aliases and interfaces.

**/*.{ts,tsx}: Ensure TypeScript type-safety; avoid any unless absolutely necessary
Create proper interfaces/types for APIs, responses, and components; place them in shared types folders rather than in the same file

Files:

  • apps/dashboard/app/(main)/websites/[id]/_components/utils/types.ts
  • packages/shared/src/lists/filters.ts
  • apps/dashboard/lib/table-tabs.tsx
  • apps/dashboard/app/(main)/websites/[id]/page.tsx
  • apps/dashboard/app/(main)/websites/[id]/_components/tabs/overview-tab.tsx
  • apps/dashboard/components/analytics/data-table.tsx
**/*.{js,ts}

📄 CodeRabbit Inference Engine (.cursor/rules/ultracite.mdc)

**/*.{js,ts}: Make sure to use the "use strict" directive in script files.
Don't have redundant "use strict".
Make sure to use the "use strict" directive in script files.

Files:

  • apps/dashboard/app/(main)/websites/[id]/_components/utils/types.ts
  • packages/shared/src/lists/filters.ts
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit Inference Engine (.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc)

**/*.{js,jsx,ts,tsx}: Use bun <file> instead of node <file> or ts-node <file>
Do not use dotenv; Bun automatically loads .env files
Do not use express; use Bun.serve() for HTTP servers
Do not use better-sqlite3; use bun:sqlite for SQLite
Do not use ioredis; use Bun.redis for Redis
Do not use pg or postgres.js; use Bun.sql for Postgres
Do not use ws; use built-in WebSocket
Prefer Bun.file over node:fs's readFile/writeFile
Use Bun.$ instead of execa for running shell commands

Files:

  • apps/dashboard/app/(main)/websites/[id]/_components/utils/types.ts
  • packages/shared/src/lists/filters.ts
  • apps/dashboard/lib/table-tabs.tsx
  • apps/dashboard/app/(main)/websites/[id]/page.tsx
  • apps/dashboard/app/(main)/websites/[id]/_components/tabs/overview-tab.tsx
  • apps/dashboard/components/analytics/data-table.tsx
**/*.{html,ts,css}

📄 CodeRabbit Inference Engine (.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc)

Use bun build <file.html|file.ts|file.css> instead of webpack or esbuild

Files:

  • apps/dashboard/app/(main)/websites/[id]/_components/utils/types.ts
  • packages/shared/src/lists/filters.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit Inference Engine (.cursor/rules/01-MUST-DO.mdc)

**/*.{ts,tsx,js,jsx}: Use console methods appropriately (e.g., console.error, console.time, console.table, console.json)
Use Dayjs instead of date-fns
Use TanStack Query for hooks; do not use SWR
When adding debugging, use JSON.stringify() for structured output

Files:

  • apps/dashboard/app/(main)/websites/[id]/_components/utils/types.ts
  • packages/shared/src/lists/filters.ts
  • apps/dashboard/lib/table-tabs.tsx
  • apps/dashboard/app/(main)/websites/[id]/page.tsx
  • apps/dashboard/app/(main)/websites/[id]/_components/tabs/overview-tab.tsx
  • apps/dashboard/components/analytics/data-table.tsx
**/*.{tsx,jsx,ts,js}

📄 CodeRabbit Inference Engine (.cursor/rules/01-MUST-DO.mdc)

When using Phosphor React icons, append 'Icon' to component names (e.g., CaretIcon, not Caret)

Files:

  • apps/dashboard/app/(main)/websites/[id]/_components/utils/types.ts
  • packages/shared/src/lists/filters.ts
  • apps/dashboard/lib/table-tabs.tsx
  • apps/dashboard/app/(main)/websites/[id]/page.tsx
  • apps/dashboard/app/(main)/websites/[id]/_components/tabs/overview-tab.tsx
  • apps/dashboard/components/analytics/data-table.tsx
**/*.{jsx,tsx}

📄 CodeRabbit Inference Engine (.cursor/rules/ultracite.mdc)

**/*.{jsx,tsx}: Don't use elements in Next.js projects.
Don't use elements in Next.js projects.
Don't pass children as props.
Don't use the return value of React.render.
Make sure all dependencies are correctly specified in React hooks.
Make sure all React hooks are called from the top level of component functions.
Don't forget key props in iterators and collection literals.
Don't destructure props inside JSX components in Solid projects.
Don't define React components inside other components.
Don't use event handlers on non-interactive elements.
Don't assign to React component props.
Don't use both children and dangerouslySetInnerHTML props on the same element.
Don't use dangerous JSX props.
Don't use Array index in keys.
Don't insert comments as text nodes.
Don't assign JSX properties multiple times.
Don't add extra closing tags for components without children.
Use <>...</> instead of ....
Watch out for possible "wrong" semicolons inside JSX elements.
Don't use accessKey attribute on any HTML element.
Don't set aria-hidden="true" on focusable elements.
Don't add ARIA roles, states, and properties to elements that don't support them.
Don't use distracting elements like or .
Only use the scope prop on elements.
Don't assign non-interactive ARIA roles to interactive HTML elements.
Make sure label elements have text content and are associated with an input.
Don't assign interactive ARIA roles to non-interactive HTML elements.
Don't assign tabIndex to non-interactive HTML elements.
Don't use positive integers for tabIndex property.
Don't include "image", "picture", or "photo" in img alt prop.
Don't use explicit role property that's the same as the implicit/default role.
Make static elements with click handlers use a valid role attribute.
Always include a title element for SVG elements.
Give all elements requiring alt text meaningful information for screen readers.
Make sure anchors have content that's accessible ...

Files:

  • apps/dashboard/lib/table-tabs.tsx
  • apps/dashboard/app/(main)/websites/[id]/page.tsx
  • apps/dashboard/app/(main)/websites/[id]/_components/tabs/overview-tab.tsx
  • apps/dashboard/components/analytics/data-table.tsx
**/*.{tsx,jsx}

📄 CodeRabbit Inference Engine (.cursor/rules/01-MUST-DO.mdc)

**/*.{tsx,jsx}: In Tailwind CSS usage, use rounded only; do not use rounded-md or rounded-xl
Use Phosphor icons, not Lucide; default to weight="duotone", use fill for arrows, and for plus icons do not set weight
In view components, decouple state management, data transformations, and API interactions from the React lifecycle
Simplify data flow in components to avoid prop drilling and callback chains
Prioritize modularity and testability in all React components
Use React Error Boundaries appropriately
Avoid useEffect in React unless it is truly critical

Files:

  • apps/dashboard/lib/table-tabs.tsx
  • apps/dashboard/app/(main)/websites/[id]/page.tsx
  • apps/dashboard/app/(main)/websites/[id]/_components/tabs/overview-tab.tsx
  • apps/dashboard/components/analytics/data-table.tsx
🧬 Code Graph Analysis (1)
apps/dashboard/app/(main)/websites/[id]/_components/utils/types.ts (1)
packages/shared/src/types/api.ts (1)
  • DynamicQueryFilter (19-34)
🔇 Additional comments (9)
packages/shared/src/lists/filters.ts (1)

9-10: LGTM: Added filter options are consistent with existing schema

Referrer and Page Path align with common filter fields and the existing filterOptions shape.

apps/dashboard/app/(main)/websites/[id]/_components/utils/types.ts (1)

36-36: LGTM: addFilter is correctly introduced on FullTabProps

Signature matches DynamicQueryFilter and fits the page-level addFilter usage.

apps/dashboard/lib/table-tabs.tsx (1)

122-123: LGTM: getFilter propagation is correct

useTableTabs correctly exposes getFilter per tab.

apps/dashboard/app/(main)/websites/[id]/page.tsx (2)

209-210: LGTM: addFilter is properly passed to tab props

This wires the tab components to trigger page-level filtering.


244-244: LGTM: Dependency list includes addFilter

Including addFilter prevents stale closures in renderTabContent.

apps/dashboard/components/analytics/data-table.tsx (1)

1229-1230: LGTM: onAddFilter is forwarded to the full-screen modal

Ensures consistent behavior across inline and full-screen views.

apps/dashboard/app/(main)/websites/[id]/_components/tabs/overview-tab.tsx (3)

147-148: Prop addition: ensure FullTabProps type is updated for addFilter.

Confirm FullTabProps includes a type-safe addFilter signature matching the constructed filter shape: { field: string; operator: 'eq' | ...; value: string }. Mismatched types will break the wiring downstream.


911-921: LGTM: onAddFilter wiring is correct and minimal.

Callback is memoized, uses the provided addFilter, and standardizes the operator to eq. Good foundation.


1106-1106: LGTM: DataTable onAddFilter connections across all tables.

Passing onAddFilter into all DataTable instances is consistent with the tab-level getFilter mappers and enables row-to-filter UX.

Also applies to: 1115-1115, 1134-1134, 1221-1221, 1253-1253, 1277-1277

Comment on lines +318 to 325
getFilter: (row: any) => {
// For referrers, use the referrer field and get the actual referrer URL
return {
field: 'referrer',
value: row.referrer
};
},
},

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.

💡 Verification agent

🧩 Analysis chain

Avoid any in getFilter mappers; align to concrete row types per tab.

Multiple getFilter lambdas use any. Per coding guidelines, avoid any in TS. Use concrete types already present in this file to improve type-safety and prevent runtime mistakes.

Apply these diffs:

-			getFilter: (row: any) => {
+			getFilter: (row: ReferrerSourceCellData) => {
 				// For referrers, use the referrer field and get the actual referrer URL
 				return {
 					field: 'referrer',
 					value: row.referrer
 				};
 			},
-			getFilter: (row: any) => ({
+			getFilter: (row: { name: string }) => ({
 				field: 'utm_source',
 				value: row.name
 			}),
-			getFilter: (row: any) => ({
+			getFilter: (row: { name: string }) => ({
 				field: 'utm_medium',
 				value: row.name
 			}),
-			getFilter: (row: any) => ({
+			getFilter: (row: { name: string }) => ({
 				field: 'utm_campaign',
 				value: row.name
 			}),
-			getFilter: (row: any) => ({
+			getFilter: (row: PageData) => ({
 				field: 'path',
 				value: row.name
 			}),
-			getFilter: (row: any) => ({
+			getFilter: (row: PageData) => ({
 				field: 'path',
 				value: row.name
 			}),
-			getFilter: (row: any) => ({
+			getFilter: (row: PageData) => ({
 				field: 'path',
 				value: row.name
 			}),
-							getFilter: (row: any) => {
+							getFilter: (row: { name: string }) => {
 								// Map display device names to filter values
 								const deviceDisplayToFilterMap: Record<string, string> = {
 									'laptop': 'mobile',
 									'tablet': 'tablet', 
 									'desktop': 'desktop',
 								};
 								return {
 									field: 'device_type',
 									value: deviceDisplayToFilterMap[row.name] || row.name
 								};
 							}
-							getFilter: (row: any) => ({
+							getFilter: (row: TechnologyData) => ({
 								field: 'browser_name',
 								value: row.name
 							}),
-							getFilter: (row: any) => ({
+							getFilter: (row: TechnologyData) => ({
 								field: 'os_name',
 								value: row.name
 							}),

Also confirm the filter field names (utm_source, utm_medium, utm_campaign, path, browser_name, os_name) match the backend API’s expected filter keys.

Also applies to: 331-335, 341-345, 351-355, 367-371, 380-384, 393-397, 1230-1241, 1262-1265, 1286-1289


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== getFilter occurrences (context) ==="
rg -n --hidden --no-ignore-vcs -S -C3 "getFilter" || true

echo
echo "=== Print overview-tab.tsx (if present) ==="
TARGET='apps/dashboard/app/(main)/websites/[id]/_components/tabs/overview-tab.tsx'
if [ -f "$TARGET" ]; then
  sed -n '1,2000p' "$TARGET" || true
else
  echo "File not found at: $TARGET"
fi

echo
echo "=== Search for suggested row types in repo ==="
rg -n --hidden --no-ignore-vcs -S -C3 "ReferrerSourceCellData|PageData|TechnologyData" || true

echo
echo "=== Search for backend filter key usages in repo ==="
rg -n --hidden --no-ignore-vcs -S -C3 "utm_source|utm_medium|utm_campaign|browser_name|os_name|path" || true

Length of output: 299150


Use concrete row types for getFilter in overview-tab.tsx (fix required)

Replace all occurrences of getFilter that use any with the concrete row types already present/imported in this file to restore type-safety. I confirmed the file defines/imports ReferrerSourceCellData, PageData and TechnologyData and the backend uses the same filter keys (utm_source/utm_medium/utm_campaign/path/browser_name/os_name/referrer/device_type).

Files/locations to update

  • apps/dashboard/app/(main)/websites/[id]/_components/tabs/overview-tab.tsx
    • referrers: ~318-325
    • utm_sources: ~331-335
    • utm_mediums: ~341-345
    • utm_campaigns: ~351-355
    • top_pages: ~367-371
    • entry_pages: ~377-383
    • exit_pages: ~390-396
    • devices tab getFilter: ~1230-1241
    • browsers tab getFilter: ~1262-1265
    • operating_systems tab getFilter: ~1286-1289

Apply these diffs:

-			getFilter: (row: any) => {
+			getFilter: (row: ReferrerSourceCellData) => {
 				// For referrers, use the referrer field and get the actual referrer URL
 				return {
 					field: 'referrer',
 					value: row.referrer
 				};
 			},
-			getFilter: (row: any) => ({
+			getFilter: (row: { name: string }) => ({
 				field: 'utm_source',
 				value: row.name
 			}),
-			getFilter: (row: any) => ({
+			getFilter: (row: { name: string }) => ({
 				field: 'utm_medium',
 				value: row.name
 			}),
-			getFilter: (row: any) => ({
+			getFilter: (row: { name: string }) => ({
 				field: 'utm_campaign',
 				value: row.name
 			}),
-			getFilter: (row: any) => ({
+			getFilter: (row: PageData) => ({
 				field: 'path',
 				value: row.name
 			}),

(Apply the same PageData change for entry_pages and exit_pages occurrences.)

-							getFilter: (row: any) => {
+							getFilter: (row: { name: string }) => {
 								// Map display device names to filter values
 								const deviceDisplayToFilterMap: Record<string, string> = {
 									'laptop': 'mobile',
 									'tablet': 'tablet', 
 									'desktop': 'desktop',
 								};
 								return {
 									field: 'device_type',
 									value: deviceDisplayToFilterMap[row.name] || row.name
 								};
 							}
-							getFilter: (row: any) => ({
+							getFilter: (row: TechnologyData) => ({
 								field: 'browser_name',
 								value: row.name
 							}),
-							getFilter: (row: any) => ({
+							getFilter: (row: TechnologyData) => ({
 								field: 'os_name',
 								value: row.name
 							}),

Notes

  • Types referenced above are already defined/imported in the file (ReferrerSourceCellData import; PageData/TechnologyData interfaces).
  • Backend filter keys (utm_source, utm_medium, utm_campaign, path, browser_name, os_name, referrer, device_type) are used elsewhere in the repo (packages/shared and rpc code), so the chosen field names align with the API.
📝 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
getFilter: (row: any) => {
// For referrers, use the referrer field and get the actual referrer URL
return {
field: 'referrer',
value: row.referrer
};
},
},
getFilter: (row: ReferrerSourceCellData) => {
// For referrers, use the referrer field and get the actual referrer URL
return {
field: 'referrer',
value: row.referrer
};
},
🤖 Prompt for AI Agents
In apps/dashboard/app/(main)/websites/[id]/_components/tabs/overview-tab.tsx
around the indicated lines (referrers ~318-325, utm_sources ~331-335,
utm_mediums ~341-345, utm_campaigns ~351-355, top_pages ~367-371, entry_pages
~377-383, exit_pages ~390-396, devices ~1230-1241, browsers ~1262-1265,
operating_systems ~1286-1289) the getFilter functions currently type their row
parameter as any; replace those any usages with the concrete types already
imported/defined in the file: use ReferrerSourceCellData for referrers, PageData
for top_pages/entry_pages/exit_pages, and TechnologyData for
devices/browsers/operating_systems/utm_* where appropriate; ensure each returned
object uses the backend filter key (referrer, path, utm_source, utm_medium,
utm_campaign, device_type, browser_name, os_name) and the row property access
matches the chosen type so TypeScript infers correct fields and restores
type-safety.

Comment on lines +367 to 371
getFilter: (row: any) => ({
field: 'path',
value: row.name
}),
},

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.

💡 Verification agent

🧩 Analysis chain

Verify page path filter value: using decoded path may not match backend expectation.

You decode the path for display, then pass row.name (decoded) to filters. If the backend expects the encoded path, filtering may fail.

If backend expects encoded values, change to pass the original (encoded) value alongside a safe decode for display, or re-encode before filtering:

-			getFilter: (row: PageData) => ({
+			getFilter: (row: PageData) => ({
 				field: 'path',
-				value: row.name
+				value: encodeURI(row.name)
 			}),

Alternatively, keep both raw and display fields when mapping the data for pages, e.g., { name: decodeURIComponent(page.name), raw: page.name }, and use row.raw for filters.

Also applies to: 380-384, 393-397


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "----- Displaying overview-tab.tsx (first 400 lines) -----"
sed -n '1,400p' "apps/dashboard/app/(main)/websites/[id]/_components/tabs/overview-tab.tsx" || true

echo
echo "----- Searching for getFilter occurrences -----"
rg -n -C3 "getFilter" --hidden --no-ignore || true

echo
echo "----- Searching for direct uses of row.name / row.raw -----"
rg -n -C3 "row\.name|row\.raw" --hidden --no-ignore || true

echo
echo "----- Searching for encode/decode usage in repo -----"
rg -n -C3 "decodeURIComponent|decodeURI|encodeURI|encodeURIComponent" --hidden --no-ignore || true

echo
echo "----- Searching for mapping that sets name: or raw: -----"
rg -n -C3 "\bname\s*:\s*|raw\s*:\s*" --hidden --no-ignore || true

echo
echo "----- Searching for PageData type or interface -----"
rg -n -C3 "type\s+PageData|interface\s+PageData|PageData\b" --hidden --no-ignore || true

Length of output: 286434


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "----- Search for 'path' used in server/query code -----"
rg -n --hidden --no-ignore "path" packages apps --glob '!**/node_modules/**' -S -g 'packages/**/src/**' -g 'apps/**/src/**' -g 'apps/**/api/**' || true

echo
echo "----- Search for 'top_pages' and where it's produced -----"
rg -n --hidden --no-ignore "top_pages" || true

echo
echo "----- Show apps/api/src/query/utils.ts around applyUrlNormalization -----"
sed -n '1,320p' apps/api/src/query/utils.ts || true

echo
echo "----- Search for server-side filter handling (look for 'filters' param usage) -----"
rg -n --hidden --no-ignore "filters" packages apps --glob '!**/node_modules/**' -S || true

echo
echo "----- Search for code that maps/normalizes page names or paths -----"
rg -n --hidden --no-ignore "applyUrlNormalization|normalize|urlNormalization|url_normalization|path_normalization|decodeURIComponent" || true

Length of output: 78219


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "---- apps/api/src/query/simple-builder.ts ----"
sed -n '1,320p' apps/api/src/query/simple-builder.ts || true

echo
echo "---- apps/api/src/query/builders/pages.ts ----"
sed -n '1,320p' apps/api/src/query/builders/pages.ts || true

echo
echo "---- packages/rpc/src/lib/analytics-utils.ts (top 320 lines) ----"
sed -n '1,320p' packages/rpc/src/lib/analytics-utils.ts || true

echo
echo "---- apps/dashboard/hooks/use-dynamic-query.ts ----"
sed -n '1,400p' apps/dashboard/hooks/use-dynamic-query.ts || true

echo
echo "---- apps/dashboard/stores/jotai/filterAtoms.ts ----"
sed -n '1,320p' apps/dashboard/stores/jotai/filterAtoms.ts || true

Length of output: 37889


Fix path filter: use the original/encoded path when adding filters

Decoded page names are only for display — the API/DB expects the normalized/encoded path. You're decoding page.name for UI then passing the decoded value to filters, which can cause filter misses. Preserve the original value and use it for filters (or re-encode as a fallback).

Files to change

  • apps/dashboard/app/(main)/websites/[id]/_components/tabs/overview-tab.tsx
    • top_pages mapping & getFilter (≈ lines 359–371)
    • entry_pages mapping & getFilter (≈ lines 372–384)
    • exit_pages mapping & getFilter (≈ lines 385–397)

Suggested change (apply same pattern to entry_pages and exit_pages):

- data: (analytics.top_pages || []).map((page: PageData) => ({
-   ...page,
-   name: decodeURIComponent(page.name),
- })),
+ data: (analytics.top_pages || []).map((page: PageData) => ({
+   ...page,
+   name: decodeURIComponent(page.name), // decoded for display
+   raw: page.name, // preserve original (encoded/normalized) value for filtering
+ })),

- getFilter: (row: any) => ({
-   field: 'path',
-   value: row.name
- }),
+ getFilter: (row: any) => ({
+   field: 'path',
+   value: row.raw ?? encodeURIComponent(row.name), // prefer original; fallback to re-encoding
+ }),

This will ensure filter values match what the backend queries against.

📝 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
getFilter: (row: any) => ({
field: 'path',
value: row.name
}),
},
data: (analytics.top_pages || []).map((page: PageData) => ({
...page,
name: decodeURIComponent(page.name), // decoded for display
raw: page.name, // preserve original (encoded/normalized) value for filtering
})),
getFilter: (row: any) => ({
field: 'path',
value: row.raw ?? encodeURIComponent(row.name), // prefer original; fallback to re-encoding
}),
},
🤖 Prompt for AI Agents
In apps/dashboard/app/(main)/websites/[id]/_components/tabs/overview-tab.tsx
around lines 359–397 (top_pages mapping & getFilter ≈ 359–371, entry_pages ≈
372–384, exit_pages ≈ 385–397), the getFilter currently uses the decoded display
name (row.name) which breaks backend filtering; change the mappings so the UI
still displays the decoded name but the getFilter uses the original/encoded path
value (e.g. row.path or row.originalPath) and, if that field is not available,
produce an encoded fallback using encodeURI/encodeURIComponent on the original
raw path before returning the filter object — apply the same pattern to
top_pages, entry_pages and exit_pages getFilter implementations so filter.value
always matches the backend-normalized path.

Comment on lines +1230 to +1241
getFilter: (row: any) => {
// Map display device names to filter values
const deviceDisplayToFilterMap: Record<string, string> = {
'laptop': 'mobile',
'tablet': 'tablet',
'desktop': 'desktop',
};
return {
field: 'device_type',
value: deviceDisplayToFilterMap[row.name] || row.name
};
}

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.

🛠️ Refactor suggestion

⚠️ Potential issue

Incorrect device mapping: 'laptop' → 'mobile'.

Device mapping appears wrong: 'laptop' should not map to 'mobile'. If your source names are typically 'mobile' | 'tablet' | 'desktop', consider normalizing and mapping those directly. Also, normalize the key to lowercase for robustness.

Apply this diff:

-							getFilter: (row: { name: string }) => {
-								// Map display device names to filter values
-								const deviceDisplayToFilterMap: Record<string, string> = {
-									'laptop': 'mobile',
-									'tablet': 'tablet', 
-									'desktop': 'desktop',
-								};
-								return {
-									field: 'device_type',
-									value: deviceDisplayToFilterMap[row.name] || row.name
-								};
-							}
+							getFilter: (row: { name: string }) => {
+								// Map display device names to filter values
+								const deviceDisplayToFilterMap: Record<string, string> = {
+									mobile: 'mobile',
+									tablet: 'tablet',
+									desktop: 'desktop',
+								};
+								const key = String(row.name).toLowerCase();
+								return {
+									field: 'device_type',
+									value: deviceDisplayToFilterMap[key] ?? key,
+								};
+							}

If 'laptop' does appear in your dataset and should be treated as 'desktop', add 'laptop': 'desktop'.

📝 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
getFilter: (row: any) => {
// Map display device names to filter values
const deviceDisplayToFilterMap: Record<string, string> = {
'laptop': 'mobile',
'tablet': 'tablet',
'desktop': 'desktop',
};
return {
field: 'device_type',
value: deviceDisplayToFilterMap[row.name] || row.name
};
}
getFilter: (row: { name: string }) => {
// Map display device names to filter values
const deviceDisplayToFilterMap: Record<string, string> = {
mobile: 'mobile',
tablet: 'tablet',
desktop: 'desktop',
};
const key = String(row.name).toLowerCase();
return {
field: 'device_type',
value: deviceDisplayToFilterMap[key] ?? key,
};
}

Comment on lines +45 to 46
getFilter?: (row: TData) => { field: string; value: string };
}

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.

🛠️ Refactor suggestion

Broaden the getFilter return type to support numeric filters

Align with DynamicQueryFilter.value and avoid over-constraining.

- getFilter?: (row: TData) => { field: string; value: string };
+ getFilter?: (row: TData) => { field: string; value: string | number };
📝 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
getFilter?: (row: TData) => { field: string; value: string };
}
getFilter?: (row: TData) => { field: string; value: string | number };
}
🤖 Prompt for AI Agents
In apps/dashboard/components/analytics/data-table.tsx around lines 45 to 46, the
getFilter return type currently forces value to a string which over-constrains
callers; change the return type to match DynamicQueryFilter.value (e.g. value:
string | number or better: value: DynamicQueryFilter['value']) and import/alias
DynamicQueryFilter if needed so numeric filters are supported and typings remain
consistent.

Comment on lines +59 to 60
onAddFilter?: (field: string, value: string, tableTitle?: string) => void;
minHeight?: string | number;

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.

🛠️ Refactor suggestion

Broaden onAddFilter to accept string | number values

Keeps the callback compatible with all filter value types and matches onRowClick’s value typing.

- onAddFilter?: (field: string, value: string, tableTitle?: string) => void;
+ onAddFilter?: (field: string, value: string | number, tableTitle?: string) => void;
📝 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
onAddFilter?: (field: string, value: string, tableTitle?: string) => void;
minHeight?: string | number;
onAddFilter?: (field: string, value: string | number, tableTitle?: string) => void;
minHeight?: string | number;
🤖 Prompt for AI Agents
In apps/dashboard/components/analytics/data-table.tsx around lines 59-60, the
onAddFilter callback currently types the value parameter as string only; change
its signature to accept string | number (onAddFilter?: (field: string, value:
string | number, tableTitle?: string) => void) so it matches onRowClick’s value
typing and supports numeric filter values, then update any local calls/usages of
onAddFilter to pass numbers without casting and adjust callers to handle number
values as needed.

Comment on lines 1018 to 1032
if (hasSubRows) {
toggleRowExpansion(row.id);
} else if (onAddFilter && row.original.name) {
// Determine the appropriate field and value based on table context
const activeTabConfig = tabs?.find(tab => tab.id === activeTab);
const filterFunc = activeTabConfig?.getFilter;
if (!filterFunc) {
return;
}

const { field, value } = filterFunc(row.original);
onAddFilter(field, value, title);
} else if (onRowClick) {
onRowClick('name', row.original.name);
}

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.

🛠️ Refactor suggestion

Remove unnecessary guard and add robust fallback for filtering

Filtering currently requires row.original.name to be truthy and silently no-ops if getFilter is missing. This causes missed filters on valid rows and inconsistent UX.

- } else if (onAddFilter && row.original.name) {
-   // Determine the appropriate field and value based on table context
-   const activeTabConfig = tabs?.find(tab => tab.id === activeTab);
-   const filterFunc = activeTabConfig?.getFilter;
-   if (!filterFunc) {
-     return;
-   }
-
-   const { field, value } = filterFunc(row.original);
-   onAddFilter(field, value, title);
+ } else if (onAddFilter) {
+   const activeTabConfig = tabs?.find((tab) => tab.id === activeTab);
+   const filter = activeTabConfig?.getFilter?.(row.original);
+   const field = filter?.field ?? 'name';
+   const value = filter?.value ?? row.original.name;
+   if (field != null && value != null) {
+     onAddFilter(field, value as string | number, title);
+   }
 } else if (onRowClick) {
-   onRowClick('name', row.original.name);
+   onRowClick('name', row.original.name);
 }
📝 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
if (hasSubRows) {
toggleRowExpansion(row.id);
} else if (onAddFilter && row.original.name) {
// Determine the appropriate field and value based on table context
const activeTabConfig = tabs?.find(tab => tab.id === activeTab);
const filterFunc = activeTabConfig?.getFilter;
if (!filterFunc) {
return;
}
const { field, value } = filterFunc(row.original);
onAddFilter(field, value, title);
} else if (onRowClick) {
onRowClick('name', row.original.name);
}
if (hasSubRows) {
toggleRowExpansion(row.id);
} else if (onAddFilter) {
const activeTabConfig = tabs?.find((tab) => tab.id === activeTab);
const filter = activeTabConfig?.getFilter?.(row.original);
const field = filter?.field ?? 'name';
const value = filter?.value ?? row.original.name;
if (field != null && value != null) {
onAddFilter(field, value as string | number, title);
}
} else if (onRowClick) {
onRowClick('name', row.original.name);
}
🤖 Prompt for AI Agents
In apps/dashboard/components/analytics/data-table.tsx around lines 1018 to 1032,
remove the unnecessary guard that requires row.original.name and instead ensure
filtering always attempts a sensible fallback: keep the hasSubRows branch, then
for the filter branch remove the row.original.name check, fetch activeTabConfig
= tabs?.find(tab => tab.id === activeTab) and const filterFunc =
activeTabConfig?.getFilter, and if filterFunc is missing use a default function
that returns { field: 'name', value: row.original.name ?? row.original.id ?? ''
}; call onAddFilter(field, value, title) only when value is non-empty to avoid
no-ops and preserve UX consistency.

Comment on lines +22 to 23
getFilter?: (row: T) => { field: string; value: string };
}

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.

🛠️ Refactor suggestion

Broaden getFilter value type to support numeric values

DynamicQueryFilter.value allows string | number | array. Constraining to string is unnecessarily narrow and may block valid filters (e.g., numeric codes). Broaden the return type for consistency.

- getFilter?: (row: T) => { field: string; value: string };
+ getFilter?: (row: T) => { field: string; value: string | number };
📝 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
getFilter?: (row: T) => { field: string; value: string };
}
getFilter?: (row: T) => { field: string; value: string | number };
}
🤖 Prompt for AI Agents
In apps/dashboard/lib/table-tabs.tsx around lines 22-23, the getFilter return
type is too narrow (value: string); broaden it to match DynamicQueryFilter.value
by changing the value type to accept string | number | (string | number)[] or,
better, reference the existing type (e.g., DynamicQueryFilter['value']) so
filters can accept numeric and array values; update the type annotation
accordingly and adjust any imports if needed.

Comment on lines +107 to 108
getFilter?: (row: T) => { field: string; value: string };
}

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.

🛠️ Refactor suggestion

Apply the same value type widening here for consistency

Keep TabConfig and SimpleTabConfig aligned on the getFilter signature.

- getFilter?: (row: T) => { field: string; value: string };
+ getFilter?: (row: T) => { field: string; value: string | number };
📝 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
getFilter?: (row: T) => { field: string; value: string };
}
getFilter?: (row: T) => { field: string; value: string | number };
}
🤖 Prompt for AI Agents
In apps/dashboard/lib/table-tabs.tsx around lines 107 to 108, the getFilter
return type is currently { field: string; value: string } and needs the same
value-type widening as the corresponding TabConfig definition; change the
signature to return { field: string; value: string | number | boolean } (or the
exact union used in TabConfig) so SimpleTabConfig and TabConfig stay aligned.

Comment on lines +13 to +20
// Table title to filter field mapping - use exact database column names
export const tableFilterMapping: Record<string, string> = {
'Traffic Sources': 'referrer',
'Pages': 'path',
'Devices': 'device_type',
'Browsers': 'browser_name',
'Operating Systems': 'os_name',
};

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.

💡 Verification agent

🧩 Analysis chain

Avoid coupling mappings to user-facing titles; prefer stable identifiers

Mapping by exact table titles is brittle (breaks with copy changes or i18n). Prefer stable IDs (e.g., enum-like string literals or a central TabId) over display labels, or at least centralize the labels to a single source of truth.

Run this script to verify all call sites depend on these exact labels and assess i18n risk:


🏁 Script executed:

#!/bin/bash
# Find usages of table titles that are used as keys here
rg -n -C2 -P "(Traffic Sources|Pages|Devices|Browsers|Operating Systems)" --type=ts --type=tsx --type=js --type=jsx

Length of output: 145


🏁 Script executed:

#!/bin/bash
echo "== show file =="
if [ -f packages/shared/src/lists/filters.ts ]; then
  sed -n '1,200p' packages/shared/src/lists/filters.ts
else
  echo "File not found: packages/shared/src/lists/filters.ts"
fi

echo
echo "== search for symbol tableFilterMapping =="
rg -n --hidden -S "tableFilterMapping" || true

echo
echo "== search for exact labels (case-sensitive) =="
for s in "Traffic Sources" "Pages" "Devices" "Browsers" "Operating Systems"; do
  echo "---- $s ----"
  rg -n -C2 -F "$s" || echo "No matches for: $s"
done

echo
echo "== search for labels (case-insensitive) =="
for s in "Traffic Sources" "Pages" "Devices" "Browsers" "Operating Systems"; do
  echo "---- $s (ci) ----"
  rg -n -C2 -i -F "$s" || echo "No matches for (ci): $s"
done

echo
echo "== search for mapped DB column names =="
for s in "referrer" "path" "device_type" "browser_name" "os_name"; do
  echo "---- $s ----"
  rg -n -C2 -F "$s" || echo "No matches for: $s"
done

Length of output: 280595


Use stable tab IDs (not user-facing titles) for tableFilterMapping

Verified findings:

  • tableFilterMapping is declared only at packages/shared/src/lists/filters.ts and I found no other references to that symbol in the repo.
  • The display titles ("Traffic Sources", "Pages", "Devices", "Browsers", "Operating Systems") are used widely in the UI and API constants (examples: apps/dashboard/.../overview-tab.tsx, performance-tab.tsx; apps/api/src/query/constants.ts).

Recommended change (small refactor):

  • Replace user-facing title keys with stable IDs (enum-like/tab IDs) or centralize labels in one place. If tableFilterMapping is not used, remove or rename it to avoid confusion.

Current (lines 13-20):

// Table title to filter field mapping - use exact database column names
export const tableFilterMapping: Record<string, string> = {
	'Traffic Sources': 'referrer',
	'Pages': 'path',
	'Devices': 'device_type',
	'Browsers': 'browser_name',
	'Operating Systems': 'os_name',
};

Suggested replacement:

// Map stable tab IDs to filter field names
export const tableFilterMapping: Record<string, string> = {
	traffic: 'referrer',
	pages: 'path',
	devices: 'device_type',
	browsers: 'browser_name',
	operating_systems: 'os_name',
};

Places to update or review:

  • packages/shared/src/lists/filters.ts (apply refactor or remove)
  • apps/api/src/query/constants.ts (uses stable ids like traffic, pages, devices)
  • apps/dashboard/app/(main)/websites/[id]/_components/tabs/overview-tab.tsx and performance-tab.tsx (UI titles)
  • Any Add Filter / onAddFilter handlers — ensure they use tab IDs (or a centralized label -> id mapping) instead of raw display strings

Action: please update the mapping to use stable IDs (or centralize display labels) and adjust any consumers to use those IDs; if mapping is unused, remove/rename it to prevent accidental coupling.

🤖 Prompt for AI Agents
In packages/shared/src/lists/filters.ts around lines 13 to 20, the mapping
currently uses user-facing table titles which are unstable; replace those keys
with stable tab IDs (e.g. traffic, pages, devices, browsers, operating_systems)
mapping to the same DB column names, then update all consumers to use the IDs
(check apps/api/src/query/constants.ts and the dashboard overview/performance
tab components and any Add Filter handlers) or remove/rename the mapping if it's
unused to avoid accidental coupling.

Comment thread packages/shared/src/lists/filters.ts
izadoesdev and others added 3 commits August 15, 2025 12:21
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
…d-table-filtering-website-overview-2025-08-15
@izadoesdev

Copy link
Copy Markdown
Member

sick, great feature, a little bit incompatible with the filter rewrite I'm doing, but very timely, gonna write them together now

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 17

🔭 Outside diff range comments (2)
apps/api/src/query/utils.ts (1)

191-205: Geo normalization: avoid writing empty code/name; return early if resolution fails

If getCountryCode(currentName) returns an empty string (e.g., unknown input), we end up setting empty country_code/country_name. Short-circuit to preserve original row when resolution fails.

Apply this diff:

 function applyGeoNormalization(data: DataRow[]): DataRow[] {
   return data.map((row) => {
-    const currentName = getString(row.name) || getString(row.country);
+    const currentName = getString(row.name) || getString(row.country);
     if (!currentName) {
       return row;
     }
     const code = getCountryCode(currentName);
+    if (!code) {
+      return row;
+    }
     const name = getCountryName(code);
     return {
       ...row,
       country_code: code,
       country_name: name,
     } as DataRow;
   });
 }
apps/dashboard/app/(main)/websites/[id]/sessions/_components/session-event-timeline.tsx (1)

52-55: Avoid array index in React keys

Fallbacking to eventIndex violates the guideline and can cause unstable renders. Prefer a deterministic composite key when event_id is absent.

Apply this diff:

-          key={event.event_id || eventIndex}
+          key={event.event_id || `${event.time}-${event.event_name}`}

If time granularity may repeat, include additional stable fields (e.g., path).

♻️ Duplicate comments (2)
packages/shared/src/lists/filters.ts (2)

13-20: Avoid mapping by user-facing titles; use stable tab IDs (duplicate of earlier feedback)

Keying tableFilterMapping by display labels is brittle (copy changes/i18n will break it). Prefer stable identifiers (e.g., tab ids). This was raised previously and still applies.

Apply this diff (example using stable ids):

-// Table title to filter field mapping - use exact database column names
-export const tableFilterMapping: Record<string, string> = {
-  'Traffic Sources': 'referrer',
-  'Pages': 'path',
-  'Devices': 'device_type',
-  'Browsers': 'browser_name',
-  'Operating Systems': 'os_name',
-};
+// Map stable tab IDs to filter field names
+export const tableFilterMapping: Record<string, string> = {
+  traffic: 'referrer',
+  pages: 'path',
+  devices: 'device_type',
+  browsers: 'browser_name',
+  operating_systems: 'os_name',
+};

If there are existing consumers expecting titles, update them to use ids or remove this mapping if unused.


22-27: Replace local device map in overview-tab with the shared deviceDisplayToFilterMap

Found a duplicate local map in the overview tab that still maps 'laptop' → 'mobile', diverging from the shared map. Remove the local map and import/use the shared one to keep a single source of truth.

Files to change:

  • packages/shared/src/lists/filters.ts — exported deviceDisplayToFilterMap ('laptop': 'laptop', 'tablet': 'tablet', 'desktop': 'desktop') (around lines 23–26).
  • apps/dashboard/app/(main)/websites/[id]/_components/tabs/overview-tab.tsx — local deviceDisplayToFilterMap (around lines 1232–1239) currently mapping 'laptop' → 'mobile' — remove this and import the shared map.

Suggested change (minimal diff):

-// Map display device names to filter values
-const deviceDisplayToFilterMap: Record<string, string> = {
-  'laptop': 'mobile',
-  'tablet': 'tablet',
-  'desktop': 'desktop',
-};
+// reuse shared device->filter map
+import { deviceDisplayToFilterMap } from 'packages/shared/src/lists/filters';

Keep usage as: value: deviceDisplayToFilterMap[row.name] || row.name

Also scan the codebase for any other local device->filter mappings and replace them with the shared export to avoid future divergence.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 2db0697 and f780616.

📒 Files selected for processing (16)
  • apps/api/src/query/builders/sessions.ts (2 hunks)
  • apps/api/src/query/utils.ts (1 hunks)
  • apps/dashboard/app/(main)/websites/[id]/_components/analytics-toolbar.tsx (1 hunks)
  • apps/dashboard/app/(main)/websites/[id]/layout.tsx (1 hunks)
  • apps/dashboard/app/(main)/websites/[id]/page.tsx (5 hunks)
  • apps/dashboard/app/(main)/websites/[id]/sessions/_components/index.ts (0 hunks)
  • apps/dashboard/app/(main)/websites/[id]/sessions/_components/session-event-timeline.tsx (1 hunks)
  • apps/dashboard/app/(main)/websites/[id]/sessions/_components/session-row.tsx (1 hunks)
  • apps/dashboard/app/(main)/websites/[id]/sessions/_components/sessions-list.tsx (1 hunks)
  • apps/dashboard/app/(main)/websites/[id]/sessions/page.tsx (2 hunks)
  • apps/dashboard/app/(main)/websites/page.tsx (0 hunks)
  • apps/dashboard/hooks/use-dynamic-query.ts (0 hunks)
  • apps/dashboard/stores/jotai/filterAtoms.ts (3 hunks)
  • apps/dashboard/stores/jotai/sessionAtoms.ts (1 hunks)
  • packages/shared/src/lists/filters.ts (1 hunks)
  • packages/shared/src/types/analytics.ts (3 hunks)
💤 Files with no reviewable changes (3)
  • apps/dashboard/hooks/use-dynamic-query.ts
  • apps/dashboard/app/(main)/websites/[id]/sessions/_components/index.ts
  • apps/dashboard/app/(main)/websites/page.tsx
🧰 Additional context used
📓 Path-based instructions (9)
**/*.{jsx,tsx}

📄 CodeRabbit Inference Engine (.cursor/rules/ultracite.mdc)

**/*.{jsx,tsx}: Don't use elements in Next.js projects.
Don't use elements in Next.js projects.
Don't pass children as props.
Don't use the return value of React.render.
Make sure all dependencies are correctly specified in React hooks.
Make sure all React hooks are called from the top level of component functions.
Don't forget key props in iterators and collection literals.
Don't destructure props inside JSX components in Solid projects.
Don't define React components inside other components.
Don't use event handlers on non-interactive elements.
Don't assign to React component props.
Don't use both children and dangerouslySetInnerHTML props on the same element.
Don't use dangerous JSX props.
Don't use Array index in keys.
Don't insert comments as text nodes.
Don't assign JSX properties multiple times.
Don't add extra closing tags for components without children.
Use <>...</> instead of ....
Watch out for possible "wrong" semicolons inside JSX elements.
Don't use accessKey attribute on any HTML element.
Don't set aria-hidden="true" on focusable elements.
Don't add ARIA roles, states, and properties to elements that don't support them.
Don't use distracting elements like or .
Only use the scope prop on elements.
Don't assign non-interactive ARIA roles to interactive HTML elements.
Make sure label elements have text content and are associated with an input.
Don't assign interactive ARIA roles to non-interactive HTML elements.
Don't assign tabIndex to non-interactive HTML elements.
Don't use positive integers for tabIndex property.
Don't include "image", "picture", or "photo" in img alt prop.
Don't use explicit role property that's the same as the implicit/default role.
Make static elements with click handlers use a valid role attribute.
Always include a title element for SVG elements.
Give all elements requiring alt text meaningful information for screen readers.
Make sure anchors have content that's accessible ...

Files:

  • apps/dashboard/app/(main)/websites/[id]/sessions/_components/session-row.tsx
  • apps/dashboard/app/(main)/websites/[id]/sessions/_components/session-event-timeline.tsx
  • apps/dashboard/app/(main)/websites/[id]/_components/analytics-toolbar.tsx
  • apps/dashboard/app/(main)/websites/[id]/layout.tsx
  • apps/dashboard/app/(main)/websites/[id]/sessions/_components/sessions-list.tsx
  • apps/dashboard/app/(main)/websites/[id]/sessions/page.tsx
  • apps/dashboard/app/(main)/websites/[id]/page.tsx
**/*.{js,ts,jsx,tsx}

📄 CodeRabbit Inference Engine (.cursor/rules/ultracite.mdc)

**/*.{js,ts,jsx,tsx}: Don't import next/document outside of pages/_document.jsx in Next.js projects.
Don't use global eval().
Don't use var.
Don't use console.
Don't use debugger.
Don't assign directly to document.cookie.
Don't use duplicate case labels.
Don't use duplicate class members.
Don't use duplicate function parameter names.
Don't use empty block statements and static blocks.
Don't use with statements in non-strict contexts.
Don't use the arguments object.
Don't use the comma operator.
Don't use unnecessary boolean casts.
Use for...of statements instead of Array.forEach.
Use arrow functions instead of function expressions.
Use Date.now() to get milliseconds since the Unix Epoch.
Use .flatMap() instead of map().flat() when possible.
Use literal property access instead of computed property access.
Don't use parseInt() or Number.parseInt() when binary, octal, or hexadecimal literals work.
Use concise optional chaining instead of chained logical expressions.
Use regular expression literals instead of the RegExp constructor when possible.
Don't use number literal object member names that aren't base 10 or use underscore separators.
Remove redundant terms from logical expressions.
Use while loops instead of for loops when you don't need initializer and update expressions.
Don't reassign const variables.
Don't use constant expressions in conditions.
Don't use Math.min and Math.max to clamp values when the result is constant.
Don't return a value from a constructor.
Don't use empty character classes in regular expression literals.
Don't use empty destructuring patterns.
Don't call global object properties as functions.
Don't declare functions and vars that are accessible outside their block.
Don't use variables and function parameters before they're declared.
Don't use 8 and 9 escape sequences in string literals.
Don't use literal numbers that lose precision.
Don't use duplicate conditions in if-else-if chains.
Don't use two keys with the same name inside objects...

Files:

  • apps/dashboard/app/(main)/websites/[id]/sessions/_components/session-row.tsx
  • apps/dashboard/app/(main)/websites/[id]/sessions/_components/session-event-timeline.tsx
  • packages/shared/src/types/analytics.ts
  • apps/api/src/query/utils.ts
  • apps/dashboard/app/(main)/websites/[id]/_components/analytics-toolbar.tsx
  • apps/api/src/query/builders/sessions.ts
  • apps/dashboard/stores/jotai/filterAtoms.ts
  • apps/dashboard/app/(main)/websites/[id]/layout.tsx
  • packages/shared/src/lists/filters.ts
  • apps/dashboard/app/(main)/websites/[id]/sessions/_components/sessions-list.tsx
  • apps/dashboard/app/(main)/websites/[id]/sessions/page.tsx
  • apps/dashboard/stores/jotai/sessionAtoms.ts
  • apps/dashboard/app/(main)/websites/[id]/page.tsx
**/*.{ts,tsx}

📄 CodeRabbit Inference Engine (.cursor/rules/ultracite.mdc)

**/*.{ts,tsx}: Don't use TypeScript enums.
Don't use TypeScript const enum.
Don't use TypeScript namespaces.
Don't use the TypeScript directive @ts-ignore.
Don't use any type.
Don't use implicit any type on variable declarations.
Don't use non-null assertions with the ! postfix operator.
Don't misuse the non-null assertion operator (!) in TypeScript files.
Don't declare empty interfaces.
Use export type for types.
Use import type for types.
Don't use primitive type aliases or misleading types.
Don't use empty type parameters in type aliases and interfaces.

**/*.{ts,tsx}: Ensure TypeScript type-safety; avoid any unless absolutely necessary
Create proper interfaces/types for APIs, responses, and components; place them in shared types folders rather than in the same file

Files:

  • apps/dashboard/app/(main)/websites/[id]/sessions/_components/session-row.tsx
  • apps/dashboard/app/(main)/websites/[id]/sessions/_components/session-event-timeline.tsx
  • packages/shared/src/types/analytics.ts
  • apps/api/src/query/utils.ts
  • apps/dashboard/app/(main)/websites/[id]/_components/analytics-toolbar.tsx
  • apps/api/src/query/builders/sessions.ts
  • apps/dashboard/stores/jotai/filterAtoms.ts
  • apps/dashboard/app/(main)/websites/[id]/layout.tsx
  • packages/shared/src/lists/filters.ts
  • apps/dashboard/app/(main)/websites/[id]/sessions/_components/sessions-list.tsx
  • apps/dashboard/app/(main)/websites/[id]/sessions/page.tsx
  • apps/dashboard/stores/jotai/sessionAtoms.ts
  • apps/dashboard/app/(main)/websites/[id]/page.tsx
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit Inference Engine (.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc)

**/*.{js,jsx,ts,tsx}: Use bun <file> instead of node <file> or ts-node <file>
Do not use dotenv; Bun automatically loads .env files
Do not use express; use Bun.serve() for HTTP servers
Do not use better-sqlite3; use bun:sqlite for SQLite
Do not use ioredis; use Bun.redis for Redis
Do not use pg or postgres.js; use Bun.sql for Postgres
Do not use ws; use built-in WebSocket
Prefer Bun.file over node:fs's readFile/writeFile
Use Bun.$ instead of execa for running shell commands

Files:

  • apps/dashboard/app/(main)/websites/[id]/sessions/_components/session-row.tsx
  • apps/dashboard/app/(main)/websites/[id]/sessions/_components/session-event-timeline.tsx
  • packages/shared/src/types/analytics.ts
  • apps/api/src/query/utils.ts
  • apps/dashboard/app/(main)/websites/[id]/_components/analytics-toolbar.tsx
  • apps/api/src/query/builders/sessions.ts
  • apps/dashboard/stores/jotai/filterAtoms.ts
  • apps/dashboard/app/(main)/websites/[id]/layout.tsx
  • packages/shared/src/lists/filters.ts
  • apps/dashboard/app/(main)/websites/[id]/sessions/_components/sessions-list.tsx
  • apps/dashboard/app/(main)/websites/[id]/sessions/page.tsx
  • apps/dashboard/stores/jotai/sessionAtoms.ts
  • apps/dashboard/app/(main)/websites/[id]/page.tsx
**/*.{tsx,jsx}

📄 CodeRabbit Inference Engine (.cursor/rules/01-MUST-DO.mdc)

**/*.{tsx,jsx}: In Tailwind CSS usage, use rounded only; do not use rounded-md or rounded-xl
Use Phosphor icons, not Lucide; default to weight="duotone", use fill for arrows, and for plus icons do not set weight
In view components, decouple state management, data transformations, and API interactions from the React lifecycle
Simplify data flow in components to avoid prop drilling and callback chains
Prioritize modularity and testability in all React components
Use React Error Boundaries appropriately
Avoid useEffect in React unless it is truly critical

Files:

  • apps/dashboard/app/(main)/websites/[id]/sessions/_components/session-row.tsx
  • apps/dashboard/app/(main)/websites/[id]/sessions/_components/session-event-timeline.tsx
  • apps/dashboard/app/(main)/websites/[id]/_components/analytics-toolbar.tsx
  • apps/dashboard/app/(main)/websites/[id]/layout.tsx
  • apps/dashboard/app/(main)/websites/[id]/sessions/_components/sessions-list.tsx
  • apps/dashboard/app/(main)/websites/[id]/sessions/page.tsx
  • apps/dashboard/app/(main)/websites/[id]/page.tsx
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit Inference Engine (.cursor/rules/01-MUST-DO.mdc)

**/*.{ts,tsx,js,jsx}: Use console methods appropriately (e.g., console.error, console.time, console.table, console.json)
Use Dayjs instead of date-fns
Use TanStack Query for hooks; do not use SWR
When adding debugging, use JSON.stringify() for structured output

Files:

  • apps/dashboard/app/(main)/websites/[id]/sessions/_components/session-row.tsx
  • apps/dashboard/app/(main)/websites/[id]/sessions/_components/session-event-timeline.tsx
  • packages/shared/src/types/analytics.ts
  • apps/api/src/query/utils.ts
  • apps/dashboard/app/(main)/websites/[id]/_components/analytics-toolbar.tsx
  • apps/api/src/query/builders/sessions.ts
  • apps/dashboard/stores/jotai/filterAtoms.ts
  • apps/dashboard/app/(main)/websites/[id]/layout.tsx
  • packages/shared/src/lists/filters.ts
  • apps/dashboard/app/(main)/websites/[id]/sessions/_components/sessions-list.tsx
  • apps/dashboard/app/(main)/websites/[id]/sessions/page.tsx
  • apps/dashboard/stores/jotai/sessionAtoms.ts
  • apps/dashboard/app/(main)/websites/[id]/page.tsx
**/*.{tsx,jsx,ts,js}

📄 CodeRabbit Inference Engine (.cursor/rules/01-MUST-DO.mdc)

When using Phosphor React icons, append 'Icon' to component names (e.g., CaretIcon, not Caret)

Files:

  • apps/dashboard/app/(main)/websites/[id]/sessions/_components/session-row.tsx
  • apps/dashboard/app/(main)/websites/[id]/sessions/_components/session-event-timeline.tsx
  • packages/shared/src/types/analytics.ts
  • apps/api/src/query/utils.ts
  • apps/dashboard/app/(main)/websites/[id]/_components/analytics-toolbar.tsx
  • apps/api/src/query/builders/sessions.ts
  • apps/dashboard/stores/jotai/filterAtoms.ts
  • apps/dashboard/app/(main)/websites/[id]/layout.tsx
  • packages/shared/src/lists/filters.ts
  • apps/dashboard/app/(main)/websites/[id]/sessions/_components/sessions-list.tsx
  • apps/dashboard/app/(main)/websites/[id]/sessions/page.tsx
  • apps/dashboard/stores/jotai/sessionAtoms.ts
  • apps/dashboard/app/(main)/websites/[id]/page.tsx
**/*.{js,ts}

📄 CodeRabbit Inference Engine (.cursor/rules/ultracite.mdc)

**/*.{js,ts}: Make sure to use the "use strict" directive in script files.
Don't have redundant "use strict".
Make sure to use the "use strict" directive in script files.

Files:

  • packages/shared/src/types/analytics.ts
  • apps/api/src/query/utils.ts
  • apps/api/src/query/builders/sessions.ts
  • apps/dashboard/stores/jotai/filterAtoms.ts
  • packages/shared/src/lists/filters.ts
  • apps/dashboard/stores/jotai/sessionAtoms.ts
**/*.{html,ts,css}

📄 CodeRabbit Inference Engine (.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc)

Use bun build <file.html|file.ts|file.css> instead of webpack or esbuild

Files:

  • packages/shared/src/types/analytics.ts
  • apps/api/src/query/utils.ts
  • apps/api/src/query/builders/sessions.ts
  • apps/dashboard/stores/jotai/filterAtoms.ts
  • packages/shared/src/lists/filters.ts
  • apps/dashboard/stores/jotai/sessionAtoms.ts
🧠 Learnings (1)
📚 Learning: 2025-08-11T10:37:28.346Z
Learnt from: CR
PR: databuddy-analytics/Databuddy#0
File: .cursor/rules/01-MUST-DO.mdc:0-0
Timestamp: 2025-08-11T10:37:28.346Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Use TanStack Query for hooks; do not use SWR

Applied to files:

  • apps/dashboard/app/(main)/websites/[id]/page.tsx
🧬 Code Graph Analysis (4)
apps/dashboard/app/(main)/websites/[id]/sessions/_components/session-row.tsx (3)
apps/dashboard/app/(main)/websites/[id]/sessions/_components/session-utils.tsx (1)
  • getCountryFlag (58-72)
apps/dashboard/app/(main)/websites/[id]/profiles/_components/profile-utils.tsx (1)
  • getCountryFlag (51-65)
packages/db/src/drizzle/schema.ts (1)
  • session (105-141)
apps/dashboard/app/(main)/websites/[id]/layout.tsx (3)
apps/dashboard/hooks/use-tracking-setup.ts (1)
  • useTrackingSetup (3-26)
apps/dashboard/stores/jotai/filterAtoms.ts (2)
  • isAnalyticsRefreshingAtom (267-267)
  • dynamicQueryFiltersAtom (274-274)
apps/dashboard/app/(main)/websites/[id]/_components/analytics-toolbar.tsx (1)
  • AnalyticsToolbar (26-225)
apps/dashboard/app/(main)/websites/[id]/sessions/_components/sessions-list.tsx (4)
apps/dashboard/stores/jotai/filterAtoms.ts (3)
  • formattedDateRangeAtom (25-35)
  • timeGranularityAtom (40-40)
  • dynamicQueryFiltersAtom (274-274)
apps/dashboard/stores/jotai/sessionAtoms.ts (2)
  • expandedSessionIdAtom (17-17)
  • getSessionPageAtom (23-38)
apps/dashboard/hooks/use-dynamic-query.ts (1)
  • useDynamicQuery (178-248)
apps/dashboard/app/(main)/websites/[id]/sessions/_components/session-row.tsx (1)
  • SessionRow (333-333)
apps/dashboard/app/(main)/websites/[id]/page.tsx (1)
apps/dashboard/stores/jotai/filterAtoms.ts (5)
  • timeGranularityAtom (40-40)
  • formattedDateRangeAtom (25-35)
  • timezoneAtom (64-66)
  • isAnalyticsRefreshingAtom (267-267)
  • dynamicQueryFiltersAtom (274-274)
🔇 Additional comments (12)
packages/shared/src/types/analytics.ts (1)

42-43: Good: stricter typing for event properties

Switching to Record<string, unknown> is a solid step to avoid any leakage and encourages proper value formatting.

packages/shared/src/lists/filters.ts (1)

9-11: New filter options look good

Adding Referrer and Page Path aligns with the new row-to-filter workflow.

apps/dashboard/app/(main)/websites/[id]/sessions/_components/session-row.tsx (1)

126-132: LGTM: prefer country_code for flag rendering, with country fallback

This ensures correct ISO code is used for flagcdn URLs while preserving robustness.

apps/dashboard/app/(main)/websites/[id]/sessions/_components/session-event-timeline.tsx (1)

19-20: Good: safer typing for properties

Changing to Record<string, unknown> improves type safety and pairs well with formatPropertyValue.

apps/dashboard/stores/jotai/filterAtoms.ts (1)

217-224: LGTM! Good defensive programming practice

Using Object.hasOwn() instead of hasOwnProperty is the modern approach and prevents issues with objects that override or lack the hasOwnProperty method.

apps/api/src/query/builders/sessions.ts (1)

183-183: WHERE clause construction — verified

buildWhereClause (apps/api/src/query/utils.ts) returns AND (${safeClauses.join(' AND ')}) when conditions are present, so the combinedWhereClause.replace('AND ', '') in apps/api/src/query/builders/sessions.ts (line ~183) will correctly strip the leading AND with the current implementation. No change required.

  • apps/api/src/query/builders/sessions.ts — line ~183: WHERE ${combinedWhereClause.replace('AND ', '')}
  • apps/api/src/query/utils.ts — export function buildWhereClause(...) { ... return \AND (${safeClauses.join(' AND ')})`; }`
apps/dashboard/app/(main)/websites/[id]/sessions/page.tsx (2)

7-20: LGTM! Clean module import refactoring

The direct import from ./components/sessions-list is cleaner than using a barrel export, especially since the index barrel was removed. The dynamic import configuration remains unchanged, preserving the loading and SSR behavior.


26-26: No change needed — parent layout includes responsive padding

Verified that the websites layout wraps pages with responsive padding, so removing local p-3 sm:p-4 lg:p-6 is safe.

  • apps/dashboard/app/(main)/websites/[id]/sessions/page.tsx — uses:
  • apps/dashboard/app/(main)/websites/[id]/layout.tsx — provides:
    (wraps {children})
apps/dashboard/app/(main)/websites/[id]/page.tsx (1)

167-171: Update dependency array after fixing addFilter

Once the addFilter function is properly defined, ensure it's correctly memoized with useCallback to prevent unnecessary re-renders of tab content.

The dependency array includes addFilter but it's not defined. After implementing the function with useCallback, the dependency array will be correct.

apps/dashboard/stores/jotai/sessionAtoms.ts (1)

23-38: LGTM! Well-structured pagination atom with proper reset handling.

The implementation correctly handles both reading the page for a specific website and resetting the pagination state. The dependency on filterDependencyAtom ensures automatic reset when filters change.

apps/dashboard/app/(main)/websites/[id]/_components/analytics-toolbar.tsx (1)

1-17: LGTM! Clean imports and proper separation of concerns.

The imports are well-organized, and the component correctly uses client-side rendering with the 'use client' directive.

apps/dashboard/app/(main)/websites/[id]/sessions/_components/sessions-list.tsx (1)

145-166: IntersectionObserver cleanup could be more robust.

The observer cleanup uses a captured ref value which might be stale.

Use the ref directly in cleanup:

 useEffect(() => {
 	const observer = new IntersectionObserver(
 		(entries) => {
 			const entry = entries[0];
 			if (entry?.isIntersecting && hasNextPage && !isLoading) {
 				setPage(page + 1);
 			}
 		},
 		{ threshold: 0.1 }
 	);

-	const currentRef = loadMoreRef.current;
-	if (currentRef) {
-		observer.observe(currentRef);
+	if (loadMoreRef.current) {
+		observer.observe(loadMoreRef.current);
 	}

 	return () => {
-		if (currentRef) {
-			observer.unobserve(currentRef);
-		}
+		if (loadMoreRef.current) {
+			observer.unobserve(loadMoreRef.current);
+		}
+		observer.disconnect();
 	};
 }, [hasNextPage, isLoading, setPage, page]);

Likely an incorrect or invalid review comment.

Comment on lines +196 to +198
plugins: {
normalizeGeo: true,
},

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.

🧹 Nitpick (assertive)

Document the normalizeGeo plugin behavior

The normalizeGeo plugin is added but its behavior and impact on the query results are not documented. Consider adding a comment explaining what this plugin does.

+		// normalizeGeo plugin enriches geo data with normalized country names and codes
 		plugins: {
 			normalizeGeo: true,
 		},
📝 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
plugins: {
normalizeGeo: true,
},
// normalizeGeo plugin enriches geo data with normalized country names and codes
plugins: {
normalizeGeo: true,
},
🤖 Prompt for AI Agents
In apps/api/src/query/builders/sessions.ts around lines 196 to 198, the
normalizeGeo plugin is enabled but undocumented; add a concise comment
immediately above this plugin config that states exactly what normalizeGeo does
(e.g., which geo fields it transforms, whether it converts to GeoJSON or ensures
{lat,lng} vs [lon,lat], any rounding/precision applied, how it affects
filtering/sorting/index usage, and any possible side effects or required
upstream formats), and include an example input → output snippet and note if it
mutates the original object or returns a normalized copy.

Comment on lines +62 to +73
const handleQuickRangeSelect = useCallback(
(range: (typeof quickRanges)[0]) => {
const now = new Date();
const start = range.hours
? dayjs(now).subtract(range.hours, 'hour').toDate()
: dayjs(now)
.subtract(range.days || 7, 'day')
.toDate();
setDateRangeAction({ startDate: start, endDate: now });
},
[setDateRangeAction]
);

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.

🧹 Nitpick (assertive)

Simplify date calculation logic to avoid duplication.

The date calculation logic for quick ranges is duplicated between the handler and the render loop.

Extract the date calculation into a shared utility:

+const calculateQuickRangeDate = (range: typeof quickRanges[0]) => {
+	const now = new Date();
+	const start = range.hours
+		? dayjs(now).subtract(range.hours, 'hour').toDate()
+		: dayjs(now).subtract(range.days || 7, 'day').toDate();
+	return { start, end: now };
+};

 const handleQuickRangeSelect = useCallback(
 	(range: (typeof quickRanges)[0]) => {
-		const now = new Date();
-		const start = range.hours
-			? dayjs(now).subtract(range.hours, 'hour').toDate()
-			: dayjs(now)
-					.subtract(range.days || 7, 'day')
-					.toDate();
-		setDateRangeAction({ startDate: start, endDate: now });
+		const { start, end } = calculateQuickRangeDate(range);
+		setDateRangeAction({ startDate: start, endDate: end });
 	},
 	[setDateRangeAction]
 );

Then use it in the render loop (line 116-130) as well.

📝 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 handleQuickRangeSelect = useCallback(
(range: (typeof quickRanges)[0]) => {
const now = new Date();
const start = range.hours
? dayjs(now).subtract(range.hours, 'hour').toDate()
: dayjs(now)
.subtract(range.days || 7, 'day')
.toDate();
setDateRangeAction({ startDate: start, endDate: now });
},
[setDateRangeAction]
);
const calculateQuickRangeDate = (range: typeof quickRanges[0]) => {
const now = new Date();
const start = range.hours
? dayjs(now).subtract(range.hours, 'hour').toDate()
: dayjs(now).subtract(range.days || 7, 'day').toDate();
return { start, end: now };
};
const handleQuickRangeSelect = useCallback(
(range: (typeof quickRanges)[0]) => {
const { start, end } = calculateQuickRangeDate(range);
setDateRangeAction({ startDate: start, endDate: end });
},
[setDateRangeAction]
);

Comment on lines +176 to +213
{selectedFilters.map((filter, index) => {
const fieldLabel = filterOptions.find(
(o) => o.value === filter.field
)?.label;
const operatorLabel = operatorOptions.find(
(o) => getOperatorShorthand(o.value) === filter.operator
)?.label;
const valueLabel = Array.isArray(filter.value)
? filter.value.join(', ')
: filter.value;

return (
<div
className="flex items-center gap-0 rounded border bg-background py-1 pr-2 pl-3 shadow-sm"
key={`filter-${index}-${filter.field}-${filter.operator}`}
>
<div className="flex items-center gap-1">
<span className="font-medium text-foreground text-sm">
{fieldLabel}
</span>
<span className="text-muted-foreground/70 text-sm">
{operatorLabel}
</span>
<span className="font-medium text-foreground text-sm">
{valueLabel}
</span>
</div>
<button
aria-label={`Remove filter ${fieldLabel} ${operatorLabel} ${valueLabel}`}
className="flex h-6 w-6 items-center justify-center rounded hover:bg-muted/50"
onClick={() => removeFilter(index)}
type="button"
>
<XIcon aria-hidden="true" className="h-3 w-3" />
</button>
</div>
);
})}

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

Add null safety for filter value display.

The filter chip rendering assumes filter.value is always defined and can be stringified. This could cause runtime errors if the value is null or undefined.

Apply this fix to handle null/undefined values:

 const valueLabel = Array.isArray(filter.value)
 	? filter.value.join(', ')
-	: filter.value;
+	: filter.value?.toString() ?? '';
📝 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
{selectedFilters.map((filter, index) => {
const fieldLabel = filterOptions.find(
(o) => o.value === filter.field
)?.label;
const operatorLabel = operatorOptions.find(
(o) => getOperatorShorthand(o.value) === filter.operator
)?.label;
const valueLabel = Array.isArray(filter.value)
? filter.value.join(', ')
: filter.value;
return (
<div
className="flex items-center gap-0 rounded border bg-background py-1 pr-2 pl-3 shadow-sm"
key={`filter-${index}-${filter.field}-${filter.operator}`}
>
<div className="flex items-center gap-1">
<span className="font-medium text-foreground text-sm">
{fieldLabel}
</span>
<span className="text-muted-foreground/70 text-sm">
{operatorLabel}
</span>
<span className="font-medium text-foreground text-sm">
{valueLabel}
</span>
</div>
<button
aria-label={`Remove filter ${fieldLabel} ${operatorLabel} ${valueLabel}`}
className="flex h-6 w-6 items-center justify-center rounded hover:bg-muted/50"
onClick={() => removeFilter(index)}
type="button"
>
<XIcon aria-hidden="true" className="h-3 w-3" />
</button>
</div>
);
})}
{selectedFilters.map((filter, index) => {
const fieldLabel = filterOptions.find(
(o) => o.value === filter.field
)?.label;
const operatorLabel = operatorOptions.find(
(o) => getOperatorShorthand(o.value) === filter.operator
)?.label;
const valueLabel = Array.isArray(filter.value)
? filter.value.join(', ')
: filter.value?.toString() ?? '';
return (
<div
className="flex items-center gap-0 rounded border bg-background py-1 pr-2 pl-3 shadow-sm"
key={`filter-${index}-${filter.field}-${filter.operator}`}
>
<div className="flex items-center gap-1">
<span className="font-medium text-foreground text-sm">
{fieldLabel}
</span>
<span className="text-muted-foreground/70 text-sm">
{operatorLabel}
</span>
<span className="font-medium text-foreground text-sm">
{valueLabel}
</span>
</div>
<button
aria-label={`Remove filter ${fieldLabel} ${operatorLabel} ${valueLabel}`}
className="flex h-6 w-6 items-center justify-center rounded hover:bg-muted/50"
onClick={() => removeFilter(index)}
type="button"
>
<XIcon aria-hidden="true" className="h-3 w-3" />
</button>
</div>
);
})}

Comment on lines +217 to +219
<Button onClick={() => onFiltersChange([])} variant="outline">
Clear all filters
</Button>

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.

🧹 Nitpick (assertive)

Consider confirming before clearing all filters.

Clearing all filters is a destructive action that users might trigger accidentally.

Consider adding a confirmation or using a more descriptive button label:

-<Button onClick={() => onFiltersChange([])} variant="outline">
-	Clear all filters
+<Button 
+	onClick={() => {
+		if (window.confirm('Are you sure you want to clear all filters?')) {
+			onFiltersChange([]);
+		}
+	}} 
+	variant="outline"
+>
+	Clear all filters
 </Button>
📝 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
<Button onClick={() => onFiltersChange([])} variant="outline">
Clear all filters
</Button>
<Button
onClick={() => {
if (window.confirm('Are you sure you want to clear all filters?')) {
onFiltersChange([]);
}
}}
variant="outline"
>
Clear all filters
</Button>
🤖 Prompt for AI Agents
In apps/dashboard/app/(main)/websites/[id]/_components/analytics-toolbar.tsx
around lines 217-219, the "Clear all filters" button performs a destructive
action without confirmation; add a confirmation step (modal or native confirm)
before calling onFiltersChange([]) so the user must explicitly confirm before
filters are cleared. Implement the confirm dialog to show a clear message and
Cancel/Confirm actions, call onFiltersChange([]) only when the user confirms,
and optionally disable the button when there are no active filters or change the
label to make the action more explicit.

Comment on lines +27 to +46
const handleRefresh = async () => {
setIsRefreshing(true);
try {
await Promise.all([
queryClient.invalidateQueries({ queryKey: ['websites', id] }),
queryClient.invalidateQueries({
queryKey: ['websites', 'isTrackingSetup', id],
}),
queryClient.invalidateQueries({ queryKey: ['dynamic-query', id] }),
queryClient.invalidateQueries({
queryKey: ['batch-dynamic-query', id],
}),
]);
toast.success('Data refreshed');
} catch {
toast.error('Failed to refresh data');
} finally {
setIsRefreshing(false);
}
};

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.

🧹 Nitpick (assertive)

Add error handling for individual query invalidations

The current implementation catches all errors together. Consider handling individual query invalidation failures to provide more granular error feedback.

 const handleRefresh = async () => {
   setIsRefreshing(true);
+  const errors: string[] = [];
   try {
-    await Promise.all([
-      queryClient.invalidateQueries({ queryKey: ['websites', id] }),
-      queryClient.invalidateQueries({
-        queryKey: ['websites', 'isTrackingSetup', id],
-      }),
-      queryClient.invalidateQueries({ queryKey: ['dynamic-query', id] }),
-      queryClient.invalidateQueries({
-        queryKey: ['batch-dynamic-query', id],
-      }),
-    ]);
+    const invalidations = [
+      { key: ['websites', id], name: 'website data' },
+      { key: ['websites', 'isTrackingSetup', id], name: 'tracking setup' },
+      { key: ['dynamic-query', id], name: 'dynamic queries' },
+      { key: ['batch-dynamic-query', id], name: 'batch queries' },
+    ];
+    
+    const results = await Promise.allSettled(
+      invalidations.map(({ key }) => 
+        queryClient.invalidateQueries({ queryKey: key })
+      )
+    );
+    
+    results.forEach((result, index) => {
+      if (result.status === 'rejected') {
+        errors.push(invalidations[index].name);
+      }
+    });
+    
+    if (errors.length > 0) {
+      toast.warning(`Partially refreshed. Failed: ${errors.join(', ')}`);
+    } else {
+      toast.success('Data refreshed');
+    }
-    toast.success('Data refreshed');
   } catch {
     toast.error('Failed to refresh data');
   } finally {
     setIsRefreshing(false);
   }
 };
📝 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 handleRefresh = async () => {
setIsRefreshing(true);
try {
await Promise.all([
queryClient.invalidateQueries({ queryKey: ['websites', id] }),
queryClient.invalidateQueries({
queryKey: ['websites', 'isTrackingSetup', id],
}),
queryClient.invalidateQueries({ queryKey: ['dynamic-query', id] }),
queryClient.invalidateQueries({
queryKey: ['batch-dynamic-query', id],
}),
]);
toast.success('Data refreshed');
} catch {
toast.error('Failed to refresh data');
} finally {
setIsRefreshing(false);
}
};
const handleRefresh = async () => {
setIsRefreshing(true);
const errors: string[] = [];
try {
const invalidations = [
{ key: ['websites', id], name: 'website data' },
{ key: ['websites', 'isTrackingSetup', id], name: 'tracking setup' },
{ key: ['dynamic-query', id], name: 'dynamic queries' },
{ key: ['batch-dynamic-query', id], name: 'batch queries' },
];
const results = await Promise.allSettled(
invalidations.map(({ key }) =>
queryClient.invalidateQueries({ queryKey: key })
)
);
results.forEach((result, index) => {
if (result.status === 'rejected') {
errors.push(invalidations[index].name);
}
});
if (errors.length > 0) {
toast.warning(`Partially refreshed. Failed: ${errors.join(', ')}`);
} else {
toast.success('Data refreshed');
}
} catch {
toast.error('Failed to refresh data');
} finally {
setIsRefreshing(false);
}
};
🤖 Prompt for AI Agents
In apps/dashboard/app/(main)/websites/[id]/layout.tsx around lines 27 to 46, the
current handleRefresh wraps all query invalidations in a single try/catch which
masks which invalidation failed; change the logic to call Promise.allSettled on
the array of queryClient.invalidateQueries promises, inspect the results to
detect per-query failures, log or collect the specific errors (including which
queryKey failed), show a success toast if all settled as fulfilled or an error
toast summarizing which specific invalidations failed, and keep setIsRefreshing
toggling in finally; this gives granular feedback while preserving the original
flow.


if (!allSessions.length) {
// Empty state
if (!(allSessions.length || isLoading)) {

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

Fix logical operator precedence issue.

The condition !(allSessions.length || isLoading) is confusing and might not work as intended.

Apply this fix for clearer logic:

-if (!(allSessions.length || isLoading)) {
+if (!allSessions.length && !isLoading) {
📝 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
if (!(allSessions.length || isLoading)) {
if (!allSessions.length && !isLoading) {
🤖 Prompt for AI Agents
In
apps/dashboard/app/(main)/websites/[id]/sessions/_components/sessions-list.tsx
around line 219, the condition using !(allSessions.length || isLoading) is
misleading; replace it with an explicit check such as allSessions.length === 0
&& !isLoading (or !allSessions.length && !isLoading) so the branch only runs
when there are no sessions and loading is false, improving clarity and
correctness.

Comment on lines +269 to +274
// --- Dynamic Query Filters (for shared package compatibility) ---
/**
* Atom for storing DynamicQueryFilter[] from @databuddy/shared
* This is used for the analytics toolbar and shared across all website pages
*/
export const dynamicQueryFiltersAtom = atom<DynamicQueryFilter[]>([]);

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.

🧹 Nitpick (assertive)

Consider implementing filter persistence

The dynamicQueryFiltersAtom is initialized as an empty array. Consider whether filters should persist across page navigations or browser sessions. You might want to add localStorage persistence using Jotai's atomWithStorage to improve user experience.

If you want to persist filters across sessions, you could refactor like this:

+import { atomWithStorage } from 'jotai/utils';
+
 // --- Dynamic Query Filters (for shared package compatibility) ---
 /**
  * Atom for storing DynamicQueryFilter[] from @databuddy/shared
  * This is used for the analytics toolbar and shared across all website pages
  */
-export const dynamicQueryFiltersAtom = atom<DynamicQueryFilter[]>([]);
+export const dynamicQueryFiltersAtom = atomWithStorage<DynamicQueryFilter[]>(
+  'analytics-dynamic-filters',
+  []
+);
🤖 Prompt for AI Agents
In apps/dashboard/stores/jotai/filterAtoms.ts around lines 269 to 274,
dynamicQueryFiltersAtom is created as a plain atom([]) which loses filters
across navigations/sessions; replace it with a persistent atom using Jotai's
atomWithStorage (import from jotai/utils) with a clear storage key (e.g.,
"dashboard:dynamicQueryFilters"), a default of [] and proper typing, and ensure
safe access in SSR by guarding localStorage (or using a no-op fallback) and
optionally adding a migration/validation step to handle malformed or
older-format data so the atom always yields a valid DynamicQueryFilter[].

Comment on lines +9 to +14
// Create a dependency atom that changes when filters/date range change
const filterDependencyAtom = atom((get) => ({
dateRange: get(formattedDateRangeAtom),
granularity: get(timeGranularityAtom),
filters: get(dynamicQueryFiltersAtom),
}));

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.

🧹 Nitpick (assertive)

Consider the performance implications of filterDependencyAtom.

The filterDependencyAtom creates a new object reference on every read, which will trigger dependent atoms to re-evaluate whenever any parent state is accessed. While this ensures proper reset behavior, it might cause unnecessary re-renders.

Consider using a more selective dependency tracking approach:

-// Create a dependency atom that changes when filters/date range change
-const filterDependencyAtom = atom((get) => ({
-	dateRange: get(formattedDateRangeAtom),
-	granularity: get(timeGranularityAtom),
-	filters: get(dynamicQueryFiltersAtom),
-}));
+// Create a dependency atom that changes when filters/date range change
+const filterDependencyAtom = atom((get) => {
+	const dateRange = get(formattedDateRangeAtom);
+	const granularity = get(timeGranularityAtom);
+	const filters = get(dynamicQueryFiltersAtom);
+	// Create a stable key for comparison
+	return `${dateRange.startDate}-${dateRange.endDate}-${granularity}-${JSON.stringify(filters)}`;
+});

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +40 to +48
// Auto-reset atoms when filters change
export const autoResetSessionStateAtom = atom(
(get) => get(filterDependencyAtom),
(_get, set) => {
// Reset all session state when filters change
set(expandedSessionIdAtom, RESET);
set(sessionPageAtom, RESET);
}
);

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.

🧹 Nitpick (assertive)

Consider adding error handling for the reset operation.

While the reset logic is correct, there's no error handling if the reset operations fail.

Consider wrapping the reset operations in a try-catch:

 export const autoResetSessionStateAtom = atom(
 	(get) => get(filterDependencyAtom),
 	(_get, set) => {
-		// Reset all session state when filters change
-		set(expandedSessionIdAtom, RESET);
-		set(sessionPageAtom, RESET);
+		try {
+			// Reset all session state when filters change
+			set(expandedSessionIdAtom, RESET);
+			set(sessionPageAtom, RESET);
+		} catch (error) {
+			console.error('Failed to reset session state:', error);
+		}
 	}
 );
📝 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
// Auto-reset atoms when filters change
export const autoResetSessionStateAtom = atom(
(get) => get(filterDependencyAtom),
(_get, set) => {
// Reset all session state when filters change
set(expandedSessionIdAtom, RESET);
set(sessionPageAtom, RESET);
}
);
// Auto-reset atoms when filters change
export const autoResetSessionStateAtom = atom(
(get) => get(filterDependencyAtom),
(_get, set) => {
try {
// Reset all session state when filters change
set(expandedSessionIdAtom, RESET);
set(sessionPageAtom, RESET);
} catch (error) {
console.error('Failed to reset session state:', error);
}
}
);
🤖 Prompt for AI Agents
In apps/dashboard/stores/jotai/sessionAtoms.ts around lines 40 to 48, the
autoResetSessionStateAtom performs two set(…, RESET) calls with no error
handling; wrap the two reset set calls inside a try-catch so any thrown errors
are caught, log a clear contextual error including the caught error (e.g.,
console.error or the app logger) and the atom names, and optionally decide
whether to rethrow or swallow the error (prefer logging and not rethrowing to
avoid breaking filter changes). Ensure both resets are attempted inside the try
block so a failure of the first does not prevent attempting the second.

Comment on lines +15 to 16
country_code: string;
}

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.

💡 Verification agent

🧩 Analysis chain

Adding required country_code may break existing producers; verify all API outputs populate it

Making country_code required on BaseSessionData tightens the contract. Ensure all API/query builders that return session/profile data now include country_code (the PR adds normalizeGeo, but confirm coverage across endpoints).

Run this script to locate producers and confirm country_code presence in API mappers/builders:


🏁 Script executed:

#!/bin/bash
# Show places where session/profile payloads are shaped
rg -n -C3 -P "(SessionData|ProfileData|BaseSessionData)\b" --type=ts --type=tsx

echo
echo "== Search for country_code in API/query builders and transformers =="
rg -n -C3 -S "country_code"

echo
echo "== Check sessions builders using normalizeGeo plugin =="
rg -n -C3 -S "normalizeGeo|applyGeoNormalization|plugins" apps/api/src

echo
echo "== Grep for profile shaping (ensure country_code is present) =="
rg -n -C3 -i "profile.*country" apps

Length of output: 21232


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== packages/shared/src/types/analytics.ts =="
sed -n '1,240p' packages/shared/src/types/analytics.ts || true

echo
echo "== apps/api/src/query/utils.ts (around DataRow & applyGeoNormalization) =="
sed -n '1,320p' apps/api/src/query/utils.ts || true

echo
echo "== apps/api/src/query/builders/sessions.ts (plugin usage & mapping) =="
sed -n '1,360p' apps/api/src/query/builders/sessions.ts || true

echo
echo "== apps/api/src/query/builders/traffic.ts (plugin usage & mapping) =="
sed -n '1,360p' apps/api/src/query/builders/traffic.ts || true

echo
echo "== apps/api/src/query/builders/performance.ts =="
sed -n '1,220p' apps/api/src/query/builders/performance.ts || true

echo
echo "== apps/api/src/query/builders/geo.ts =="
sed -n '1,220p' apps/api/src/query/builders/geo.ts || true

echo
echo "== Search for other places shaping session/profile objects =="
rg -n -C2 "country_code" || true
rg -n -C2 "session_id:|country: getCountryCode|profile.*country" || true

Length of output: 58011


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Type references (SessionData / ProfileData / BaseSessionData) =="
rg -n --hidden -S '\b(SessionData|ProfileData|BaseSessionData)\b' || true

echo
echo "== Object literals containing session_id (candidates for producers) =="
rg -n --hidden -S 'session_id\s*:' -g '!**/node_modules/**' -C2 || true

echo
echo "== Places assigning country_code in object literals =="
rg -n --hidden -S 'country_code\s*:' -g '!**/node_modules/**' -C2 || true

echo
echo "== getCountryCode / getCountryName usages =="
rg -n --hidden -S 'getCountryCode\(|getCountryName\(' || true

echo
echo "== apps/dashboard/hooks/use-dynamic-query.ts (mapping around profiles/sessions) =="
sed -n '660,840p' apps/dashboard/hooks/use-dynamic-query.ts || true

echo
echo "== apps/api/src - search for returned JSON shaping responses =="
rg -n --hidden -S 'return\s+{\s*.*session_id|res\.json\(|res\.status\(' apps/api/src -g '!**/node_modules/**' -C2 || true

echo
echo "== session_list builder and applyPlugins link check =="
sed -n '1,260p' apps/api/src/query/builders/sessions.ts || true
sed -n '1,260p' apps/api/src/query/utils.ts || true

Length of output: 36035


Make producers emit country_code — BaseSessionData now requires it

BaseSessionData now requires country_code. I checked the codebase: query plugins (applyGeoNormalization in apps/api/src/query/utils.ts) will populate country_code for DataRow outputs and several builders enable normalizeGeo (session_list, performance_by_country, geo.country), but frontend mappers still omit a dedicated country_code property and will fail TypeScript checks.

Files to fix (minimal changes needed):

  • apps/dashboard/hooks/use-dynamic-query.ts
    • transformSessionsData: add country_code: countryCode to the returned session objects.
    • transformProfilesData: add country_code to the top-level ProfileData and to each sessionData object (use getCountryCode(...) — you currently set country to the code but not country_code).
  • apps/api/src/query/utils.ts
    • applyGeoNormalization already sets country_code on DataRow — confirm the query execution path calls applyPlugins(...) before responses are serialized so API responses include country_code.
  • Verify other producers that construct session/profile objects (search results showed other mappings and builders) and add country_code where they construct objects directly.

Suggested minimal patch examples:

  • In apps/dashboard/hooks/use-dynamic-query.ts, replace
    country: countryCode,
    country_name: countryName,
    with
    country: countryCode,
    country_code: countryCode,
    country_name: countryName,
    (and the same for profile/session objects inside transformProfilesData).
🤖 Prompt for AI Agents
packages/shared/src/types/analytics.ts (around lines 15-16) and related
frontend/back-end mappers: BaseSessionData now requires country_code but several
producers omit it; update producers to emit country_code. In
apps/dashboard/hooks/use-dynamic-query.ts update transformSessionsData to
include country_code: countryCode on returned session objects, and update
transformProfilesData to include country_code on the top-level ProfileData and
on each sessionData object (use getCountryCode(...) the same way you set
country). In apps/api/src/query/utils.ts confirm applyGeoNormalization runs
before response serialization so DataRow already contains country_code; if not,
ensure applyPlugins(...) / applyGeoNormalization is invoked prior to building
API responses. Search other builders/mappers that construct session/profile
objects and add country_code where objects are created to satisfy the
BaseSessionData type.

@izadoesdev
izadoesdev merged commit 8c18071 into databuddy-analytics:main Aug 15, 2025
0 of 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.

2 participants