Skip to content

Advanced Analytics Filters#3457

Open
pepeladeira wants to merge 57 commits intomainfrom
feat/advanced-filters
Open

Advanced Analytics Filters#3457
pepeladeira wants to merge 57 commits intomainfrom
feat/advanced-filters

Conversation

@pepeladeira
Copy link
Collaborator

@pepeladeira pepeladeira commented Feb 11, 2026

Adds support for advanced filter operators in analytics: IS, IS NOT.

Changes

Frontend:

  • Multi-select filter UI with checkboxes
  • Live filtering (applies immediately)
  • Operator toggle between "is" and "is not"
  • Smart display: shows individual values or count ("2 countries")
  • URL format: ?country=US,BR or ?country=-US for negation

Backend:

  • Shared parse function in @dub/utils for frontend/backend consistency
  • Zod schema transforms for filter parameters
  • Generic SQL builder for all filter types
  • Backward compatible with existing single-value filters

Tinybird:

  • Updated 5 pipes (count, timeseries, group_by, group_by_link_metadata, events)
  • Advanced filter SQL for 11 filter types (country, city, device, browser, etc.)
  • JSON-based filter system for scalability

Summary by CodeRabbit

  • New Features

    • Advanced analytics filters: operator-aware (is / is not / is one of / is not one of), multi-value/exclusion support across domains, tags, folders, UTMs, partner/group/tenant, and metadata.
    • UI: per-filter removal, operator toggling, multi-value selection, and loader-aware active-filters display; filter components and hooks expose new handlers.
  • Improvements

    • Unified parsing and normalization for legacy filter formats; richer query parsing and Tinybird pipe-driven filtering; public types expanded for flexible filter shapes.
  • Tests

    • Large additions of unit and integration tests for filter parsing, normalization, and analytics endpoints.

pepeladeira and others added 30 commits February 1, 2026 20:27
…al, getFirstFilterValue in partner-profile/export routes
@pepeladeira pepeladeira changed the base branch from main to stablecoins February 13, 2026 10:06
@pepeladeira pepeladeira changed the base branch from stablecoins to main February 13, 2026 10:06
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@apps/web/ui/analytics/use-analytics-filters.tsx`:
- Around line 1004-1018: The loader placeholders in activeFiltersWithStreaming
produce numeric values (values: [i]) which mismatches the string[] shape used
elsewhere; update the streaming branch that creates loader filter entries (the
Array.from(...).map callback) to emit string values (e.g., String(i) or `${i}`)
so the resulting loader filter has values: string[] and matches the
type/behavior expected by consumers of activeFiltersWithStreaming, leaving
activeFilters and streaming logic unchanged.
🧹 Nitpick comments (3)
apps/web/ui/analytics/use-analytics-filters.tsx (3)

121-123: Unnecessary indirection — parseFilterParam is a trivial wrapper.

parseFilterParam just delegates to parseFilterValue with no additional logic or closure state. You can call parseFilterValue directly at every call site and remove this useCallback.


165-165: Deeply nested ternary chain harms readability.

This single line chains four ternary operators to resolve the param value for destructured keys. A lookup map would be clearer and easier to extend.

♻️ Suggested refactor
-      const value = params[filter] || (filter === "domain" ? domain : filter === "tagIds" ? tagIds : filter === "root" ? root : filter === "folderId" ? folderId : undefined);
+      const destructuredParams: Record<string, string | undefined> = {
+        domain,
+        tagIds,
+        root,
+        folderId,
+      };
+      const value = params[filter] || destructuredParams[filter];

879-887: URL construction for link filter assumes no protocol in value.

new URL(\https://${value}`)will produce an invalid URL likehttps://https://example.com/...` if value ever contains a protocol prefix. Currently, linkConstructor({ pretty: true }) strips the protocol, so this works — but it's an implicit coupling. A guard or comment would prevent a subtle breakage if linkConstructor's behavior changes.

🛡️ Defensive guard
     } else if (key === "link") {
+      const linkUrl = value.startsWith("http") ? value : `https://${value}`;
       queryParams({
         set: {
-          domain: new URL(`https://${value}`).hostname,
-          key: new URL(`https://${value}`).pathname.slice(1) || "_root",
+          domain: new URL(linkUrl).hostname,
+          key: new URL(linkUrl).pathname.slice(1) || "_root",
         },
         del: "page",
         scroll: false,
       });

Comment on lines 1004 to 1018
const activeFiltersWithStreaming = useMemo(
() => [
...activeFilters,
...(streaming && !activeFilters.length
? Array.from({ length: 2 }, (_, i) => i).map((i) => ({
() => {
return [
...activeFilters,
...(streaming && !activeFilters.length
? Array.from({ length: 2 }, (_, i) => i).map((i) => ({
key: "loader",
value: i,
values: [i],
operator: "IS" as const,
}))
: []),
],
: []),
];
},
[activeFilters, streaming],
);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Loader placeholder uses numeric values while all other filters use string arrays.

The loader entries use values: [i] where i is a number (0 or 1), while every other filter builds values as string[]. If any downstream consumer iterates activeFiltersWithStreaming and calls string methods on values, this will throw at runtime. Consider using string values for consistency.

Proposed fix
-          ? Array.from({ length: 2 }, (_, i) => i).map((i) => ({
+          ? Array.from({ length: 2 }, (_, i) => i).map((i) => ({
             key: "loader",
-            values: [i],
+            values: [String(i)],
             operator: "IS" as const,
           }))
🤖 Prompt for AI Agents
In `@apps/web/ui/analytics/use-analytics-filters.tsx` around lines 1004 - 1018,
The loader placeholders in activeFiltersWithStreaming produce numeric values
(values: [i]) which mismatches the string[] shape used elsewhere; update the
streaming branch that creates loader filter entries (the Array.from(...).map
callback) to emit string values (e.g., String(i) or `${i}`) so the resulting
loader filter has values: string[] and matches the type/behavior expected by
consumers of activeFiltersWithStreaming, leaving activeFilters and streaming
logic unchanged.

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.

3 participants