From d2d14f6de0393a9adfb2dd782e3dd6f3ecbb228a Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Thu, 12 Mar 2026 16:18:52 -0700 Subject: [PATCH] choose correct Metricsbar based on component type, fix metric types list based on component type, fix chart previews reanimating. fix hostname broken query on channel query --- .../[boardId]/BoardComponentRenderer.tsx | 9 ++- .../boards/[boardId]/BoardComponentSelect.tsx | 34 +++++---- .../boards/[boardId]/BoardEditColumn.tsx | 6 +- .../boards/[boardId]/BoardViewColumn.tsx | 2 +- .../(main)/boards/boardComponentRegistry.tsx | 75 ++++++++++++++----- .../(main)/links/[linkId]/LinkMetricsBar.tsx | 2 +- .../pixels/[pixelId]/PixelMetricsBar.tsx | 2 +- src/queries/sql/getChannelExpandedMetrics.ts | 4 +- src/queries/sql/getChannelMetrics.ts | 5 +- 9 files changed, 97 insertions(+), 42 deletions(-) diff --git a/src/app/(main)/boards/[boardId]/BoardComponentRenderer.tsx b/src/app/(main)/boards/[boardId]/BoardComponentRenderer.tsx index f6449fbd4..ec2a30df1 100644 --- a/src/app/(main)/boards/[boardId]/BoardComponentRenderer.tsx +++ b/src/app/(main)/boards/[boardId]/BoardComponentRenderer.tsx @@ -6,9 +6,11 @@ import { getComponentDefinition } from '../boardComponentRegistry'; function BoardComponentRendererComponent({ config, websiteId, + entityType, }: { config: BoardComponentConfig; websiteId?: string; + entityType?: string; }) { const definition = getComponentDefinition(config.type); @@ -20,7 +22,8 @@ function BoardComponentRendererComponent({ ); } - const Component = definition.component; + const Component = + (entityType && definition.componentByEntityType?.[entityType]) || definition.component; if (!websiteId && definition.requiresWebsite !== false) { return ( @@ -36,7 +39,9 @@ function BoardComponentRendererComponent({ export const BoardComponentRenderer = memo( BoardComponentRendererComponent, (prevProps, nextProps) => - prevProps.websiteId === nextProps.websiteId && prevProps.config === nextProps.config, + prevProps.websiteId === nextProps.websiteId && + prevProps.entityType === nextProps.entityType && + prevProps.config === nextProps.config, ); BoardComponentRenderer.displayName = 'BoardComponentRenderer'; diff --git a/src/app/(main)/boards/[boardId]/BoardComponentSelect.tsx b/src/app/(main)/boards/[boardId]/BoardComponentSelect.tsx index 16c77d269..3a03ffea0 100644 --- a/src/app/(main)/boards/[boardId]/BoardComponentSelect.tsx +++ b/src/app/(main)/boards/[boardId]/BoardComponentSelect.tsx @@ -177,14 +177,16 @@ export function BoardComponentSelect({ onSelect(config); }; - const previewConfig: BoardComponentConfig | null = selectedDef - ? { - type: selectedDef.type, - title, - description, - props: { ...selectedDef.defaultProps, ...configValues }, - } - : null; + const previewConfig: BoardComponentConfig | null = useMemo( + () => + selectedDef + ? { + type: selectedDef.type, + props: { ...selectedDef.defaultProps, ...configValues }, + } + : null, + [selectedDef, configValues], + ); const canSave = !!selectedDef && isSelectedDefSupported && (!needsWebsite || !!resolvedEntityId); const availableDefinitions = useMemo( @@ -274,12 +276,16 @@ export function BoardComponentSelect({ )} @@ -364,7 +370,7 @@ export function BoardComponentSelect({ Preview {hasSelectedEntity && previewConfig && (!needsWebsite || resolvedEntityId) ? ( - + ) : ( diff --git a/src/app/(main)/boards/[boardId]/BoardEditColumn.tsx b/src/app/(main)/boards/[boardId]/BoardEditColumn.tsx index 32cfc7c26..0a89ea25c 100644 --- a/src/app/(main)/boards/[boardId]/BoardEditColumn.tsx +++ b/src/app/(main)/boards/[boardId]/BoardEditColumn.tsx @@ -42,14 +42,14 @@ export function BoardEditColumn({ const boardType = getBoardType(board); const { entityType: boardEntityType, entityId: boardEntityId } = getBoardEntity(board); const definition = component ? getComponentDefinition(component.type) : undefined; - const { entityId } = getResolvedComponentEntity(board, component); + const { entityType, entityId } = getResolvedComponentEntity(board, component); const renderedComponent = useMemo(() => { if (!component || (!entityId && definition?.requiresWebsite !== false)) { return null; } - return ; - }, [component, definition?.requiresWebsite, entityId]); + return ; + }, [component, definition?.requiresWebsite, entityId, entityType]); const handleSelect = (config: BoardComponentConfig) => { onSetComponent(id, config); diff --git a/src/app/(main)/boards/[boardId]/BoardViewColumn.tsx b/src/app/(main)/boards/[boardId]/BoardViewColumn.tsx index 580209442..5bb2f8b65 100644 --- a/src/app/(main)/boards/[boardId]/BoardViewColumn.tsx +++ b/src/app/(main)/boards/[boardId]/BoardViewColumn.tsx @@ -43,7 +43,7 @@ export function BoardViewColumn({ {description && {description}} - + diff --git a/src/app/(main)/boards/boardComponentRegistry.tsx b/src/app/(main)/boards/boardComponentRegistry.tsx index 31d2f2c81..2af1c7006 100644 --- a/src/app/(main)/boards/boardComponentRegistry.tsx +++ b/src/app/(main)/boards/boardComponentRegistry.tsx @@ -1,5 +1,6 @@ -import type { ComponentType } from 'react'; import { TextBlock } from '@/app/(main)/boards/TextBlock'; +import { LinkMetricsBar } from '@/app/(main)/links/[linkId]/LinkMetricsBar'; +import { PixelMetricsBar } from '@/app/(main)/pixels/[pixelId]/PixelMetricsBar'; import { WebsiteChart } from '@/app/(main)/websites/[websiteId]/WebsiteChart'; import { WebsiteMetricsBar } from '@/app/(main)/websites/[websiteId]/WebsiteMetricsBar'; import { @@ -15,12 +16,14 @@ import { EventsChart } from '@/components/metrics/EventsChart'; import { MetricsTable } from '@/components/metrics/MetricsTable'; import { WeeklyTraffic } from '@/components/metrics/WeeklyTraffic'; import { WorldMap } from '@/components/metrics/WorldMap'; +import type { ComponentType } from 'react'; export interface ConfigField { name: string; label: string; type: 'select' | 'number' | 'text' | 'textarea'; options?: { label: string; value: string }[]; + optionsByEntityType?: Record; defaultValue?: any; } @@ -31,6 +34,7 @@ export interface ComponentDefinition { category: string; icon: ComponentType; component: ComponentType; + componentByEntityType?: Record>; defaultProps?: Record; configFields?: ConfigField[]; requiresWebsite?: boolean; @@ -44,31 +48,60 @@ export const CATEGORIES = [ ] as const; const METRIC_TYPES = [ - { label: 'Pages', value: 'path' }, - { label: 'Entry pages', value: 'entry' }, - { label: 'Exit pages', value: 'exit' }, - { label: 'Referrers', value: 'referrer' }, - { label: 'Channels', value: 'channel' }, - { label: 'Browsers', value: 'browser' }, + { label: 'Path', value: 'path' }, + { label: 'Entry page', value: 'entry' }, + { label: 'Exit page', value: 'exit' }, + { label: 'Title', value: 'title' }, + { label: 'Query', value: 'query' }, + { label: 'Referrer', value: 'referrer' }, + { label: 'Channel', value: 'channel' }, + { label: 'Country', value: 'country' }, + { label: 'Region', value: 'region' }, + { label: 'City', value: 'city' }, + { label: 'Browser', value: 'browser' }, { label: 'OS', value: 'os' }, - { label: 'Devices', value: 'device' }, - { label: 'Countries', value: 'country' }, - { label: 'Regions', value: 'region' }, - { label: 'Cities', value: 'city' }, - { label: 'Languages', value: 'language' }, - { label: 'Screens', value: 'screen' }, - { label: 'Query parameters', value: 'query' }, - { label: 'Page titles', value: 'title' }, - { label: 'Hosts', value: 'host' }, - { label: 'Events', value: 'event' }, + { label: 'Device', value: 'device' }, + { label: 'Language', value: 'language' }, + { label: 'Screen', value: 'screen' }, + { label: 'UTM Source', value: 'utmSource' }, + { label: 'UTM Medium', value: 'utmMedium' }, + { label: 'UTM Campaign', value: 'utmCampaign' }, + { label: 'UTM Content', value: 'utmContent' }, + { label: 'UTM Term', value: 'utmTerm' }, + { label: 'Event', value: 'event' }, + { label: 'Hostname', value: 'hostname' }, ]; +const PIXEL_LINK_METRIC_TYPES = METRIC_TYPES.filter(({ value }) => + [ + 'referrer', + 'country', + 'region', + 'city', + 'browser', + 'os', + 'device', + 'query', + 'utmSource', + 'utmMedium', + 'utmCampaign', + 'utmContent', + 'utmTerm', + ].includes(value), +); + const LIMIT_OPTIONS = [ { label: '5', value: '5' }, { label: '10', value: '10' }, { label: '20', value: '20' }, ]; +const PixelMetricsBarAdapter = ({ websiteId }: { websiteId?: string }) => + websiteId ? : null; + +const LinkMetricsBarAdapter = ({ websiteId }: { websiteId?: string }) => + websiteId ? : null; + const componentDefinitions: ComponentDefinition[] = [ // Overview { @@ -78,6 +111,10 @@ const componentDefinitions: ComponentDefinition[] = [ category: 'overview', icon: PanelTop, component: WebsiteMetricsBar, + componentByEntityType: { + pixel: PixelMetricsBarAdapter, + link: LinkMetricsBarAdapter, + }, }, { type: 'WebsiteChart', @@ -103,6 +140,10 @@ const componentDefinitions: ComponentDefinition[] = [ label: 'Metric type', type: 'select', options: METRIC_TYPES, + optionsByEntityType: { + pixel: PIXEL_LINK_METRIC_TYPES, + link: PIXEL_LINK_METRIC_TYPES, + }, defaultValue: 'path', }, { diff --git a/src/app/(main)/links/[linkId]/LinkMetricsBar.tsx b/src/app/(main)/links/[linkId]/LinkMetricsBar.tsx index ec07962c6..e8c946bfe 100644 --- a/src/app/(main)/links/[linkId]/LinkMetricsBar.tsx +++ b/src/app/(main)/links/[linkId]/LinkMetricsBar.tsx @@ -14,7 +14,7 @@ export function LinkMetricsBar({ }) { const { isAllTime } = useDateRange(); const { t, labels } = useMessages(); - const { data, isLoading, isFetching, error } = useWebsiteStatsQuery(linkId); + const { data, isLoading, isFetching, error } = useWebsiteStatsQuery({ websiteId: linkId }); const { pageviews, visitors, visits, comparison } = data || {}; diff --git a/src/app/(main)/pixels/[pixelId]/PixelMetricsBar.tsx b/src/app/(main)/pixels/[pixelId]/PixelMetricsBar.tsx index 3a46291e1..d17bc78c6 100644 --- a/src/app/(main)/pixels/[pixelId]/PixelMetricsBar.tsx +++ b/src/app/(main)/pixels/[pixelId]/PixelMetricsBar.tsx @@ -14,7 +14,7 @@ export function PixelMetricsBar({ }) { const { isAllTime } = useDateRange(); const { t, labels } = useMessages(); - const { data, isLoading, isFetching, error } = useWebsiteStatsQuery(pixelId); + const { data, isLoading, isFetching, error } = useWebsiteStatsQuery({ websiteId: pixelId }); const { pageviews, visitors, visits, comparison } = data || {}; diff --git a/src/queries/sql/getChannelExpandedMetrics.ts b/src/queries/sql/getChannelExpandedMetrics.ts index c3201dfa6..c43e2544b 100644 --- a/src/queries/sql/getChannelExpandedMetrics.ts +++ b/src/queries/sql/getChannelExpandedMetrics.ts @@ -60,6 +60,7 @@ async function relationalQuery( website_event.utm_source, website_event.session_id, website_event.visit_id, + website_event.hostname, count(*) c, min(website_event.created_at) min_time, max(website_event.created_at) max_time @@ -77,7 +78,8 @@ async function relationalQuery( website_event.utm_medium, website_event.utm_source, website_event.session_id, - website_event.visit_id), + website_event.visit_id, + website_event.hostname), channels as ( select case diff --git a/src/queries/sql/getChannelMetrics.ts b/src/queries/sql/getChannelMetrics.ts index 487c5dd74..da28c0492 100644 --- a/src/queries/sql/getChannelMetrics.ts +++ b/src/queries/sql/getChannelMetrics.ts @@ -39,7 +39,8 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) { website_event.url_query, website_event.utm_medium, website_event.utm_source, - website_event.session_id + website_event.session_id, + website_event.hostname from website_event ${cohortQuery} ${excludeBounceQuery} @@ -61,7 +62,7 @@ async function relationalQuery(websiteId: string, filters: QueryFilters) { when ${toPostgresLikeClause('referrer_domain', EMAIL_DOMAINS)} or utm_medium ilike '%mail%' then 'email' when ${toPostgresLikeClause('referrer_domain', SHOPPING_DOMAINS)} or utm_medium ilike '%shop%' then concat(prefix, 'Shopping') when ${toPostgresLikeClause('referrer_domain', VIDEO_DOMAINS)} or utm_medium ilike '%video%' then concat(prefix, 'Video') - wwhen referrer_domain != regexp_replace(hostname, '^www.', '') and referrer_domain != '' then 'referral' + when referrer_domain != regexp_replace(hostname, '^www.', '') and referrer_domain != '' then 'referral' else '' end AS x, count(distinct session_id) y from prefix