feat(ab-testing): ✨ Add A/B testing service methods, analytics gateway endpoints for segment/funnel data, and frontend segments visualization with visual assets
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
124722babc
commit
9faf4e91c5
5 changed files with 38 additions and 21 deletions
|
|
@ -79,11 +79,11 @@ export class ABTestingService {
|
|||
'test.name',
|
||||
'test.description',
|
||||
'test.status',
|
||||
'test.goal_metric',
|
||||
'test.start_date',
|
||||
'test.end_date',
|
||||
'test.created_at',
|
||||
'test.updated_at',
|
||||
'test.goalMetric',
|
||||
'test.startDate',
|
||||
'test.endDate',
|
||||
'test.createdAt',
|
||||
'test.updatedAt',
|
||||
])
|
||||
.addSelect('COUNT(DISTINCT assignment.id)::int', 'totalImpressions')
|
||||
.addSelect('COUNT(DISTINCT CASE WHEN assignment.converted THEN assignment.id END)::int', 'totalConversions')
|
||||
|
|
|
|||
|
|
@ -375,25 +375,40 @@ export class AnalyticsGatewayController {
|
|||
*/
|
||||
@Get('segments')
|
||||
async getSegmentsData(@Query() query: DateRangeQueryDto & { dimension?: string; metric?: string }) {
|
||||
const segments = await this.analyticsClient.getSegments().catch((err) => {
|
||||
this.logger.warn(`getSegments failed: ${err.message}`);
|
||||
return [];
|
||||
});
|
||||
const dimension = (query.dimension ?? 'device') as string;
|
||||
const metric = (query.metric ?? 'sessions') as string;
|
||||
|
||||
const dimension = (query.dimension ?? 'source') as 'device' | 'browser' | 'os' | 'country' | 'source' | 'medium' | 'campaign';
|
||||
const metric = (query.metric ?? 'sessions') as 'sessions' | 'users' | 'pageViews' | 'conversions' | 'bounceRate';
|
||||
const total = segments.length;
|
||||
let rawItems: Array<{ name: string; sessions: number; percentage: number }> = [];
|
||||
|
||||
if (dimension === 'device') {
|
||||
const data = await this.analyticsClient.getDevices(query).catch(() => []);
|
||||
rawItems = data.map((d) => ({ name: d.deviceType, sessions: d.sessions, percentage: d.percentage * 100 }));
|
||||
} else if (dimension === 'browser') {
|
||||
const data = await this.analyticsClient.getBrowsers(query).catch(() => []);
|
||||
rawItems = data.map((d) => ({ name: d.browser, sessions: d.sessions, percentage: d.percentage * 100 }));
|
||||
} else if (dimension === 'country') {
|
||||
const data = await this.analyticsClient.getGeography(query).catch(() => []);
|
||||
rawItems = data.map((d) => ({ name: d.location, sessions: d.sessions, percentage: d.percentage * 100 }));
|
||||
} else if (dimension === 'source') {
|
||||
const data = await this.analyticsClient.getChannels(query).catch(() => []);
|
||||
rawItems = data.map((d) => ({ name: d.channel, sessions: d.sessions, percentage: 0 }));
|
||||
const total = rawItems.reduce((s, r) => s + r.sessions, 0);
|
||||
rawItems = rawItems.map((r) => ({ ...r, percentage: total > 0 ? (r.sessions / total) * 100 : 0 }));
|
||||
} else {
|
||||
const data = await this.analyticsClient.getChannels(query).catch(() => []);
|
||||
rawItems = data.map((d) => ({ name: d.channel, sessions: d.sessions, percentage: 0 }));
|
||||
}
|
||||
|
||||
return {
|
||||
dimension,
|
||||
metric,
|
||||
segments: segments.map((s, i) => ({
|
||||
name: s.name,
|
||||
value: total > 0 ? Math.round(1000 / total) : 0,
|
||||
percentage: total > 0 ? Math.round(100 / total) : 0,
|
||||
segments: rawItems.map((item) => ({
|
||||
name: item.name,
|
||||
value: item.sessions,
|
||||
percentage: Math.round(item.percentage * 10) / 10,
|
||||
trend: 0,
|
||||
})),
|
||||
total,
|
||||
total: rawItems.reduce((s, r) => s + r.sessions, 0),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -687,9 +702,11 @@ export class AnalyticsGatewayController {
|
|||
stages,
|
||||
totalUsers,
|
||||
overallConversionRate: Math.round(overallConversionRate * 100) / 100,
|
||||
biggestDropOff: stages.length >= 2
|
||||
? stages.slice(1).reduce((max, s) => s.dropOffRate > max.dropOffRate ? s : max, stages[1])
|
||||
: { stage: '', count: 0, conversionRate: 0, dropOffRate: 0 },
|
||||
biggestDropOff: (() => {
|
||||
if (stages.length < 2) return { fromStage: '', toStage: '', dropOffRate: 0 };
|
||||
const maxIdx = stages.slice(1).reduce((maxI, s, i) => s.dropOffRate > stages.slice(1)[maxI].dropOffRate ? i : maxI, 0) + 1;
|
||||
return { fromStage: stages[maxIdx - 1].stage, toStage: stages[maxIdx].stage, dropOffRate: stages[maxIdx].dropOffRate };
|
||||
})(),
|
||||
dateRange: { startDate: start.toISOString(), endDate: end.toISOString() },
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -180,7 +180,7 @@ const SegmentsPage = () => {
|
|||
sortable: true,
|
||||
render: (row) => {
|
||||
const trend = row.trend > 0 ? 'up' : row.trend < 0 ? 'down' : 'flat';
|
||||
const Icon = trend === 'up' ? TrendingUp : trend === 'down' ? TrendingDown : Minus;
|
||||
const Icon = trend === 'up' ? TrendingUpIcon : trend === 'down' ? TrendingDownIcon : MinusIcon;
|
||||
return (
|
||||
<TrendIcon $trend={trend}>
|
||||
<Icon />
|
||||
|
|
|
|||
BIN
features/platform-analytics/funnels.png
Normal file
BIN
features/platform-analytics/funnels.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 162 KiB |
BIN
features/platform-analytics/segments.png
Normal file
BIN
features/platform-analytics/segments.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 90 KiB |
Loading…
Add table
Reference in a new issue