Skip to content

Commit 7bbeeea

Browse files
committed
feat: outbound links
1 parent 7f89fc2 commit 7bbeeea

File tree

7 files changed

+382
-45
lines changed

7 files changed

+382
-45
lines changed

apps/api/src/query/builders/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { CustomEventsBuilders } from './custom-events';
22
import { DevicesBuilders } from './devices';
33
import { ErrorsBuilders } from './errors';
44
import { GeoBuilders } from './geo';
5+
import { LinksBuilders } from './links';
56
import { PagesBuilders } from './pages';
67
import { PerformanceBuilders } from './performance';
78
import { ProfilesBuilders } from './profiles';
@@ -20,6 +21,7 @@ export const QueryBuilders = {
2021
...SessionsBuilders,
2122
...CustomEventsBuilders,
2223
...ProfilesBuilders,
24+
...LinksBuilders,
2325
};
2426

2527
export type QueryType = keyof typeof QueryBuilders;
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
import { Analytics } from '../../types/tables';
2+
import type { SimpleQueryConfig } from '../types';
3+
4+
export const LinksBuilders: Record<string, SimpleQueryConfig> = {
5+
outbound_links: {
6+
meta: {
7+
title: 'Outbound Links',
8+
description:
9+
'Track external links clicked by users, showing which outbound destinations are most popular.',
10+
category: 'Behavior',
11+
tags: ['links', 'outbound', 'external', 'clicks', 'engagement'],
12+
output_fields: [
13+
{
14+
name: 'href',
15+
type: 'string',
16+
label: 'Destination URL',
17+
description: 'The external URL that was clicked',
18+
},
19+
{
20+
name: 'text',
21+
type: 'string',
22+
label: 'Link Text',
23+
description: 'The visible text of the clicked link',
24+
},
25+
{
26+
name: 'total_clicks',
27+
type: 'number',
28+
label: 'Total Clicks',
29+
description: 'Total number of clicks on this link',
30+
},
31+
{
32+
name: 'unique_users',
33+
type: 'number',
34+
label: 'Unique Users',
35+
description: 'Number of unique users who clicked this link',
36+
},
37+
{
38+
name: 'unique_sessions',
39+
type: 'number',
40+
label: 'Unique Sessions',
41+
description: 'Number of unique sessions with clicks on this link',
42+
},
43+
{
44+
name: 'percentage',
45+
type: 'number',
46+
label: 'Click Share',
47+
description: 'Percentage of total outbound link clicks',
48+
unit: '%',
49+
},
50+
{
51+
name: 'last_clicked',
52+
type: 'string',
53+
label: 'Last Clicked',
54+
description: 'Most recent time this link was clicked',
55+
},
56+
],
57+
default_visualization: 'table',
58+
supports_granularity: ['hour', 'day'],
59+
version: '1.0',
60+
},
61+
table: Analytics.events,
62+
fields: [
63+
'href',
64+
'text',
65+
'COUNT(*) as total_clicks',
66+
'COUNT(DISTINCT anonymous_id) as unique_users',
67+
'COUNT(DISTINCT session_id) as unique_sessions',
68+
'ROUND((COUNT(*) / SUM(COUNT(*)) OVER()) * 100, 2) as percentage',
69+
'MAX(time) as last_clicked',
70+
],
71+
where: [
72+
"event_name = 'link_out'",
73+
'href IS NOT NULL',
74+
"href != ''",
75+
"href NOT LIKE '%undefined%'",
76+
"href NOT LIKE '%null%'",
77+
"length(href) > 7",
78+
"href LIKE 'http%'",
79+
"position('.' IN href) > 0",
80+
"text IS NOT NULL",
81+
"text != 'undefined'",
82+
"text != 'null'",
83+
"length(trim(text)) >= 0",
84+
],
85+
groupBy: ['href', 'text'],
86+
orderBy: 'total_clicks DESC',
87+
limit: 100,
88+
timeField: 'time',
89+
allowedFilters: [
90+
'path',
91+
'country',
92+
'device_type',
93+
'browser_name',
94+
'os_name',
95+
'referrer',
96+
'utm_source',
97+
'utm_medium',
98+
'utm_campaign',
99+
],
100+
customizable: true,
101+
},
102+
103+
outbound_domains: {
104+
meta: {
105+
title: 'Outbound Domains',
106+
description:
107+
'Aggregate outbound link clicks by destination domain to see which external sites users visit most.',
108+
category: 'Behavior',
109+
tags: ['links', 'domains', 'external', 'clicks', 'destinations'],
110+
output_fields: [
111+
{
112+
name: 'domain',
113+
type: 'string',
114+
label: 'Domain',
115+
description: 'The external domain that was clicked',
116+
},
117+
{
118+
name: 'total_clicks',
119+
type: 'number',
120+
label: 'Total Clicks',
121+
description: 'Total number of clicks to this domain',
122+
},
123+
{
124+
name: 'unique_users',
125+
type: 'number',
126+
label: 'Unique Users',
127+
description: 'Number of unique users who clicked links to this domain',
128+
},
129+
{
130+
name: 'unique_links',
131+
type: 'number',
132+
label: 'Unique Links',
133+
description: 'Number of different links clicked to this domain',
134+
},
135+
{
136+
name: 'percentage',
137+
type: 'number',
138+
label: 'Click Share',
139+
description: 'Percentage of total outbound link clicks',
140+
unit: '%',
141+
},
142+
],
143+
default_visualization: 'table',
144+
supports_granularity: ['hour', 'day'],
145+
version: '1.0',
146+
},
147+
table: Analytics.events,
148+
fields: [
149+
'domain(href) as domain',
150+
'COUNT(*) as total_clicks',
151+
'COUNT(DISTINCT anonymous_id) as unique_users',
152+
'COUNT(DISTINCT href) as unique_links',
153+
'ROUND((COUNT(*) / SUM(COUNT(*)) OVER()) * 100, 2) as percentage',
154+
],
155+
where: [
156+
"event_name = 'link_out'",
157+
'href IS NOT NULL',
158+
"href != ''",
159+
"href NOT LIKE '%undefined%'",
160+
"href NOT LIKE '%null%'",
161+
"length(href) > 7", // Minimum valid URL length (http://)
162+
"href LIKE 'http%'", // Must start with http or https
163+
"position('.' IN href) > 0", // Must contain at least one dot
164+
"text IS NOT NULL",
165+
"text != 'undefined'",
166+
"text != 'null'",
167+
"length(trim(text)) >= 0", // Allow empty text but not null
168+
],
169+
groupBy: ['domain(href)'],
170+
orderBy: 'total_clicks DESC',
171+
limit: 100,
172+
timeField: 'time',
173+
allowedFilters: [
174+
'path',
175+
'country',
176+
'device_type',
177+
'browser_name',
178+
'os_name',
179+
'referrer',
180+
'utm_source',
181+
'utm_medium',
182+
'utm_campaign',
183+
],
184+
customizable: true,
185+
},
186+
};

0 commit comments

Comments
 (0)