Skip to content

Commit 82e9192

Browse files
authored
Removed UTM dropdowns from Analytics Growth pages (#25193)
This reverts commit 3c15df6. We've decided to go in a different direction for the UX here after seeing how this works live. There is a lot of noise in the UTM parameters that a real site sees in production, and the current UX doesn't do much to help the user make sense of it.
1 parent 6428908 commit 82e9192

File tree

16 files changed

+115
-491
lines changed

16 files changed

+115
-491
lines changed

apps/admin-x-framework/src/api/stats.ts

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -80,19 +80,6 @@ export type PostGrowthStatsResponseType = {
8080
meta: Meta;
8181
};
8282

83-
export type UtmGrowthStatItem = {
84-
utm_value: string;
85-
utm_type: string;
86-
free_members: number;
87-
paid_members: number;
88-
mrr: number;
89-
};
90-
91-
export type UtmGrowthStatsResponseType = {
92-
stats: UtmGrowthStatItem[];
93-
meta: Meta;
94-
};
95-
9683
export type MrrHistoryItem = {
9784
date: string;
9885
mrr: number;
@@ -216,7 +203,6 @@ const newsletterStatsDataType = 'NewsletterStatsResponseType';
216203
const newsletterSubscriberStatsDataType = 'NewsletterSubscriberStatsResponseType';
217204

218205
const postGrowthStatsDataType = 'PostGrowthStatsResponseType';
219-
const utmGrowthStatsDataType = 'UtmGrowthStatsResponseType';
220206
const mrrHistoryDataType = 'MrrHistoryResponseType';
221207
const topPostViewsDataType = 'TopPostViewsResponseType';
222208
const subscriptionStatsDataType = 'SubscriptionStatsResponseType';
@@ -245,12 +231,6 @@ export const usePostGrowthStats = createQueryWithId<PostGrowthStatsResponseType>
245231
dataType: postGrowthStatsDataType,
246232
path: id => `/stats/posts/${id}/growth`
247233
});
248-
249-
export const useUtmGrowthStats = createQuery<UtmGrowthStatsResponseType>({
250-
dataType: utmGrowthStatsDataType,
251-
path: '/stats/utm-growth/'
252-
});
253-
254234
export const useMrrHistory = createQuery<MrrHistoryResponseType>({
255235
dataType: mrrHistoryDataType,
256236
path: '/stats/mrr/'

apps/posts/src/views/PostAnalytics/Growth/components/GrowthSources.tsx

Lines changed: 35 additions & 127 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,12 @@
1-
import React, {useState} from 'react';
1+
import React from 'react';
22
import SourceIcon from '../../components/SourceIcon';
3-
import {BaseSourceData, ProcessedSourceData, extendSourcesWithPercentages, processSources, useNavigate, useParams} from '@tryghost/admin-x-framework';
4-
import {Button, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, EmptyIndicator, LucideIcon, Separator, Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger, SkeletonTable, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, UtmCampaignTabs, UtmCampaignType, UtmTabType, cn, formatNumber, getUtmType} from '@tryghost/shade';
3+
import {BaseSourceData, ProcessedSourceData, extendSourcesWithPercentages, processSources, useNavigate} from '@tryghost/admin-x-framework';
4+
import {Button, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, EmptyIndicator, LucideIcon, Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, cn, formatNumber} from '@tryghost/shade';
55
import {useAppContext} from '@src/App';
6-
import {useGlobalData} from '@src/providers/PostAnalyticsContext';
7-
import {useUtmGrowthStats} from '@tryghost/admin-x-framework/api/stats';
86

97
// Default source icon URL - apps can override this
108
const DEFAULT_SOURCE_ICON_URL = 'https://www.google.com/s2/favicons?domain=ghost.org&sz=64';
119

12-
// Number of top sources to show in the preview card
13-
const TOP_SOURCES_PREVIEW_LIMIT = 10;
14-
1510
interface SourcesTableProps {
1611
data: ProcessedSourceData[] | null;
1712
mode: 'visits' | 'growth';
@@ -112,72 +107,16 @@ export const GrowthSources: React.FC<SourcesCardProps> = ({
112107
}) => {
113108
const {appSettings} = useAppContext();
114109
const navigate = useNavigate();
115-
const {data: globalData} = useGlobalData();
116-
const {postId} = useParams();
117-
const [selectedTab, setSelectedTab] = useState<UtmTabType>('sources');
118-
const [selectedCampaign, setSelectedCampaign] = useState<UtmCampaignType>('');
119-
120-
// Check if UTM tracking is enabled in labs
121-
const utmTrackingEnabled = globalData?.labs?.utmTracking || false;
122-
123-
const shouldFetchUtmData = utmTrackingEnabled
124-
&& selectedTab === 'campaigns'
125-
&& !!selectedCampaign
126-
&& !!postId;
127-
128-
const utmType = getUtmType(selectedCampaign);
129-
const {data: utmData, isFetching: isUtmFetching} = useUtmGrowthStats({
130-
searchParams: {
131-
utm_type: utmType,
132-
post_id: postId || ''
133-
},
134-
enabled: shouldFetchUtmData
135-
});
136-
137-
// Select and transform the appropriate data based on current view
138-
const displayData = React.useMemo(() => {
139-
const isShowingCampaigns = selectedTab === 'campaigns' && selectedCampaign;
140-
141-
if (!isShowingCampaigns) {
142-
return data;
143-
}
144-
145-
if (!utmData?.stats) {
146-
return null;
147-
}
148-
149-
return utmData.stats.map(item => ({
150-
...item,
151-
source: item.utm_value || '(not set)'
152-
}));
153-
}, [data, utmData, selectedTab, selectedCampaign]);
154-
155110
// Process and group sources data with pre-computed icons and display values
156-
const processedData = React.useMemo((): ProcessedSourceData[] => {
157-
// UTM campaigns: UTM values are not domains, so don't show icons or links
158-
if (selectedTab === 'campaigns' && selectedCampaign && displayData) {
159-
return displayData.map(item => ({
160-
source: String(item.source || '(not set)'),
161-
visits: 0,
162-
isDirectTraffic: false,
163-
iconSrc: '',
164-
displayName: String(item.source || '(not set)'),
165-
linkUrl: undefined,
166-
free_members: item.free_members || 0,
167-
paid_members: item.paid_members || 0,
168-
mrr: item.mrr || 0
169-
}));
170-
}
171-
172-
// For regular sources, use the standard processing
111+
const processedData = React.useMemo(() => {
173112
return processSources({
174-
data: displayData,
113+
data,
175114
mode,
176115
siteUrl,
177116
siteIcon,
178117
defaultSourceIconUrl
179118
});
180-
}, [displayData, siteUrl, siteIcon, mode, defaultSourceIconUrl, selectedTab, selectedCampaign]);
119+
}, [data, siteUrl, siteIcon, mode, defaultSourceIconUrl]);
181120

182121
// Extend processed data with percentage values for visits mode
183122
const extendedData = React.useMemo(() => {
@@ -188,29 +127,25 @@ export const GrowthSources: React.FC<SourcesCardProps> = ({
188127
});
189128
}, [processedData, totalVisitors, mode]);
190129

191-
const topSources = extendedData.slice(0, TOP_SOURCES_PREVIEW_LIMIT);
130+
const topSources = extendedData.slice(0, 10);
192131

193-
// Check if original source data has content (for tab visibility)
194-
const hasAnySourceData = data && data.length > 0;
195-
196-
// Generate title and description based on mode, tab, and campaign
197-
const cardTitle = selectedTab === 'campaigns' && selectedCampaign ? selectedCampaign : title;
132+
// Generate description based on mode and range
198133
const cardDescription = description || (
199134
mode === 'growth'
200135
? 'Where did your growth come from?'
201136
: `How readers found your ${range ? 'site' : 'post'}${range && getPeriodText ? ` ${getPeriodText(range)}` : ''}`
202137
);
203138

204-
const sheetTitle = selectedTab === 'campaigns' && selectedCampaign ? selectedCampaign : (mode === 'growth' ? 'Sources' : 'Top sources');
139+
const sheetTitle = mode === 'growth' ? 'Sources' : 'Top sources';
205140
const sheetDescription = mode === 'growth'
206141
? 'Where did your growth come from?'
207142
: `How readers found your ${range ? 'site' : 'post'}${range && getPeriodText ? ` ${getPeriodText(range)}` : ''}`;
208143

209144
return (
210145
<Card className={cn('group/datalist w-full max-w-[calc(100vw-64px)] overflow-x-auto sidebar:max-w-[calc(100vw-64px-280px)]', className)} data-testid='top-sources-card'>
211-
{hasAnySourceData &&
146+
{topSources.length <= 0 &&
212147
<CardHeader>
213-
<CardTitle>{cardTitle}</CardTitle>
148+
<CardTitle>{title}</CardTitle>
214149
<CardDescription>{cardDescription}</CardDescription>
215150
</CardHeader>
216151
}
@@ -228,60 +163,33 @@ export const GrowthSources: React.FC<SourcesCardProps> = ({
228163
>
229164
<LucideIcon.Activity />
230165
</EmptyIndicator>
166+
) : topSources.length > 0 ? (
167+
<SourcesTable
168+
data={topSources}
169+
defaultSourceIconUrl={defaultSourceIconUrl}
170+
getPeriodText={getPeriodText}
171+
headerStyle='card'
172+
mode={mode}
173+
range={range}
174+
>
175+
<CardHeader>
176+
<CardTitle>{title}</CardTitle>
177+
<CardDescription>{cardDescription}</CardDescription>
178+
</CardHeader>
179+
</SourcesTable>
231180
) : (
232-
<>
233-
{utmTrackingEnabled && mode === 'growth' && hasAnySourceData && (
234-
<>
235-
<div className='mb-4'>
236-
<UtmCampaignTabs
237-
selectedCampaign={selectedCampaign}
238-
selectedTab={selectedTab}
239-
onCampaignChange={setSelectedCampaign}
240-
onTabChange={setSelectedTab}
241-
/>
242-
</div>
243-
<Separator />
244-
</>
245-
)}
246-
{(selectedTab === 'campaigns' && selectedCampaign && isUtmFetching) ? (
247-
<SkeletonTable className='mt-3' />
248-
) : (hasAnySourceData || (selectedTab === 'campaigns' && selectedCampaign)) ? (
249-
<>
250-
{topSources.length > 0 ? (
251-
<SourcesTable
252-
data={topSources}
253-
defaultSourceIconUrl={defaultSourceIconUrl}
254-
getPeriodText={getPeriodText}
255-
mode={mode}
256-
range={range}
257-
/>
258-
) : (
259-
<div className='py-20 text-center text-sm text-gray-700' data-testid='empty-sources-indicator'>
260-
<EmptyIndicator
261-
className='h-full'
262-
description={mode === 'growth' && `No data available for this view`}
263-
title={`No ${selectedTab === 'campaigns' && selectedCampaign ? selectedCampaign.toLowerCase() : 'sources'} data available`}
264-
>
265-
<LucideIcon.UserPlus strokeWidth={1.5} />
266-
</EmptyIndicator>
267-
</div>
268-
)}
269-
</>
270-
) : (
271-
<div className='py-20 text-center text-sm text-gray-700' data-testid='empty-sources-indicator'>
272-
<EmptyIndicator
273-
className='h-full'
274-
description={mode === 'growth' && `Once someone signs up on this post, sources will show here`}
275-
title={`No sources data available ${getPeriodText ? getPeriodText(range) : ''}`}
276-
>
277-
<LucideIcon.UserPlus strokeWidth={1.5} />
278-
</EmptyIndicator>
279-
</div>
280-
)}
281-
</>
181+
<div className='py-20 text-center text-sm text-gray-700'>
182+
<EmptyIndicator
183+
className='h-full'
184+
description={mode === 'growth' && `Once someone signs up on this post, sources will show here`}
185+
title={`No sources data available ${getPeriodText ? getPeriodText(range) : ''}`}
186+
>
187+
<LucideIcon.UserPlus strokeWidth={1.5} />
188+
</EmptyIndicator>
189+
</div>
282190
)}
283191
</CardContent>
284-
{extendedData.length > TOP_SOURCES_PREVIEW_LIMIT &&
192+
{extendedData.length > 10 &&
285193
<CardFooter>
286194
<Sheet>
287195
<SheetTrigger asChild>

apps/posts/src/views/PostAnalytics/Web/components/Sources.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React, {useState} from 'react';
22
import SourceIcon from '../../components/SourceIcon';
33
import {BaseSourceData, ProcessedSourceData, extendSourcesWithPercentages, processSources, useTinybirdQuery} from '@tryghost/admin-x-framework';
4-
import {Button, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, DataList, DataListBar, DataListBody, DataListHead, DataListHeader, DataListItemContent, DataListItemValue, DataListItemValueAbs, DataListItemValuePerc, DataListRow, HTable, LucideIcon, Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger, SkeletonTable, UtmCampaignTabs, UtmCampaignType, UtmTabType, formatNumber, formatPercentage, formatQueryDate, getRangeDates} from '@tryghost/shade';
4+
import {Button, CampaignType, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, DataList, DataListBar, DataListBody, DataListHead, DataListHeader, DataListItemContent, DataListItemValue, DataListItemValueAbs, DataListItemValuePerc, DataListRow, HTable, LucideIcon, Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger, SkeletonTable, TabType, UtmCampaignTabs, formatNumber, formatPercentage, formatQueryDate, getRangeDates} from '@tryghost/shade';
55
import {getAudienceQueryParam} from '../../components/AudienceSelect';
66
import {getPeriodText} from '@src/utils/chart-helpers';
77
import {useGlobalData} from '@src/providers/PostAnalyticsContext';
@@ -101,8 +101,8 @@ export const Sources: React.FC<SourcesCardProps> = ({
101101
topSourcesLimit = 10
102102
}) => {
103103
const {data: globalData, statsConfig, audience, post, isPostLoading} = useGlobalData();
104-
const [selectedTab, setSelectedTab] = useState<UtmTabType>('sources');
105-
const [selectedCampaign, setSelectedCampaign] = useState<UtmCampaignType>('');
104+
const [selectedTab, setSelectedTab] = useState<TabType>('sources');
105+
const [selectedCampaign, setSelectedCampaign] = useState<CampaignType>('');
106106

107107
// Check if UTM tracking is enabled in labs
108108
const utmTrackingEnabled = globalData?.labs?.utmTracking || false;
@@ -132,7 +132,7 @@ export const Sources: React.FC<SourcesCardProps> = ({
132132
}, [isPostLoading, post, statsConfig?.id, startDate, endDate, timezone, audience]);
133133

134134
// Map campaign types to endpoints
135-
const campaignEndpointMap: Record<UtmCampaignType, string> = {
135+
const campaignEndpointMap: Record<CampaignType, string> = {
136136
'': '',
137137
'UTM sources': 'api_top_utm_sources',
138138
'UTM mediums': 'api_top_utm_mediums',
@@ -160,7 +160,7 @@ export const Sources: React.FC<SourcesCardProps> = ({
160160
}
161161

162162
// Map UTM field names to the generic key name
163-
const utmKeyMap: Record<UtmCampaignType, string> = {
163+
const utmKeyMap: Record<CampaignType, string> = {
164164
'': '',
165165
'UTM sources': 'utm_source',
166166
'UTM mediums': 'utm_medium',

0 commit comments

Comments
 (0)