feat: add dashboard dimension filtering and custom event properties
Dashboard filtering: FilterBar pills, AddFilterDropdown with dimension/ operator/value steps, URL-serialized filters, all SWR hooks filter-aware. Custom event properties: pulse.track() accepts props object, EventProperties panel with auto-discovered key tabs and value bar charts, clickable goal rows. Updated changelog with both features under v0.13.0-alpha.
This commit is contained in:
@@ -120,6 +120,7 @@ function buildQuery(
|
||||
interval?: string
|
||||
countryLimit?: number
|
||||
sort?: string
|
||||
filters?: string
|
||||
},
|
||||
auth?: AuthParams
|
||||
): string {
|
||||
@@ -130,6 +131,7 @@ function buildQuery(
|
||||
if (opts.interval) params.append('interval', opts.interval)
|
||||
if (opts.countryLimit != null) params.append('country_limit', opts.countryLimit.toString())
|
||||
if (opts.sort) params.append('sort', opts.sort)
|
||||
if (opts.filters) params.append('filters', opts.filters)
|
||||
if (auth) appendAuthParams(params, auth)
|
||||
const query = params.toString()
|
||||
return query ? `?${query}` : ''
|
||||
@@ -137,8 +139,8 @@ function buildQuery(
|
||||
|
||||
/** Factory for endpoints that return an array nested under a response key. */
|
||||
function createListFetcher<T>(path: string, field: string, defaultLimit = 10) {
|
||||
return (siteId: string, startDate?: string, endDate?: string, limit = defaultLimit): Promise<T[]> =>
|
||||
apiRequest<Record<string, T[]>>(`/sites/${siteId}/${path}${buildQuery({ startDate, endDate, limit })}`)
|
||||
return (siteId: string, startDate?: string, endDate?: string, limit = defaultLimit, filters?: string): Promise<T[]> =>
|
||||
apiRequest<Record<string, T[]>>(`/sites/${siteId}/${path}${buildQuery({ startDate, endDate, limit, filters })}`)
|
||||
.then(r => r?.[field] || [])
|
||||
}
|
||||
|
||||
@@ -160,8 +162,8 @@ export const getCampaigns = createListFetcher<CampaignStat>('campaigns', 'campai
|
||||
|
||||
// ─── Stats & Realtime ───────────────────────────────────────────────
|
||||
|
||||
export function getStats(siteId: string, startDate?: string, endDate?: string): Promise<Stats> {
|
||||
return apiRequest<Stats>(`/sites/${siteId}/stats${buildQuery({ startDate, endDate })}`)
|
||||
export function getStats(siteId: string, startDate?: string, endDate?: string, filters?: string): Promise<Stats> {
|
||||
return apiRequest<Stats>(`/sites/${siteId}/stats${buildQuery({ startDate, endDate, filters })}`)
|
||||
}
|
||||
|
||||
export function getPublicStats(siteId: string, startDate?: string, endDate?: string, auth?: AuthParams): Promise<Stats> {
|
||||
@@ -178,8 +180,8 @@ export function getPublicRealtime(siteId: string, auth?: AuthParams): Promise<Re
|
||||
|
||||
// ─── Daily Stats ────────────────────────────────────────────────────
|
||||
|
||||
export function getDailyStats(siteId: string, startDate?: string, endDate?: string, interval?: string): Promise<DailyStat[]> {
|
||||
return apiRequest<{ stats: DailyStat[] }>(`/sites/${siteId}/daily${buildQuery({ startDate, endDate, interval })}`)
|
||||
export function getDailyStats(siteId: string, startDate?: string, endDate?: string, interval?: string, filters?: string): Promise<DailyStat[]> {
|
||||
return apiRequest<{ stats: DailyStat[] }>(`/sites/${siteId}/daily${buildQuery({ startDate, endDate, interval, filters })}`)
|
||||
.then(r => r?.stats || [])
|
||||
}
|
||||
|
||||
@@ -302,8 +304,8 @@ export interface DashboardGoalsData {
|
||||
goal_counts: GoalCountStat[]
|
||||
}
|
||||
|
||||
export function getDashboardOverview(siteId: string, startDate?: string, endDate?: string, interval?: string): Promise<DashboardOverviewData> {
|
||||
return apiRequest<DashboardOverviewData>(`/sites/${siteId}/dashboard/overview${buildQuery({ startDate, endDate, interval })}`)
|
||||
export function getDashboardOverview(siteId: string, startDate?: string, endDate?: string, interval?: string, filters?: string): Promise<DashboardOverviewData> {
|
||||
return apiRequest<DashboardOverviewData>(`/sites/${siteId}/dashboard/overview${buildQuery({ startDate, endDate, interval, filters })}`)
|
||||
}
|
||||
|
||||
export function getPublicDashboardOverview(
|
||||
@@ -313,8 +315,8 @@ export function getPublicDashboardOverview(
|
||||
return apiRequest<DashboardOverviewData>(`/public/sites/${siteId}/dashboard/overview${buildQuery({ startDate, endDate, interval }, { password, captcha })}`)
|
||||
}
|
||||
|
||||
export function getDashboardPages(siteId: string, startDate?: string, endDate?: string, limit = 10): Promise<DashboardPagesData> {
|
||||
return apiRequest<DashboardPagesData>(`/sites/${siteId}/dashboard/pages${buildQuery({ startDate, endDate, limit })}`)
|
||||
export function getDashboardPages(siteId: string, startDate?: string, endDate?: string, limit = 10, filters?: string): Promise<DashboardPagesData> {
|
||||
return apiRequest<DashboardPagesData>(`/sites/${siteId}/dashboard/pages${buildQuery({ startDate, endDate, limit, filters })}`)
|
||||
}
|
||||
|
||||
export function getPublicDashboardPages(
|
||||
@@ -324,8 +326,8 @@ export function getPublicDashboardPages(
|
||||
return apiRequest<DashboardPagesData>(`/public/sites/${siteId}/dashboard/pages${buildQuery({ startDate, endDate, limit }, { password, captcha })}`)
|
||||
}
|
||||
|
||||
export function getDashboardLocations(siteId: string, startDate?: string, endDate?: string, limit = 10, countryLimit = 250): Promise<DashboardLocationsData> {
|
||||
return apiRequest<DashboardLocationsData>(`/sites/${siteId}/dashboard/locations${buildQuery({ startDate, endDate, limit, countryLimit })}`)
|
||||
export function getDashboardLocations(siteId: string, startDate?: string, endDate?: string, limit = 10, countryLimit = 250, filters?: string): Promise<DashboardLocationsData> {
|
||||
return apiRequest<DashboardLocationsData>(`/sites/${siteId}/dashboard/locations${buildQuery({ startDate, endDate, limit, countryLimit, filters })}`)
|
||||
}
|
||||
|
||||
export function getPublicDashboardLocations(
|
||||
@@ -335,8 +337,8 @@ export function getPublicDashboardLocations(
|
||||
return apiRequest<DashboardLocationsData>(`/public/sites/${siteId}/dashboard/locations${buildQuery({ startDate, endDate, limit, countryLimit }, { password, captcha })}`)
|
||||
}
|
||||
|
||||
export function getDashboardDevices(siteId: string, startDate?: string, endDate?: string, limit = 10): Promise<DashboardDevicesData> {
|
||||
return apiRequest<DashboardDevicesData>(`/sites/${siteId}/dashboard/devices${buildQuery({ startDate, endDate, limit })}`)
|
||||
export function getDashboardDevices(siteId: string, startDate?: string, endDate?: string, limit = 10, filters?: string): Promise<DashboardDevicesData> {
|
||||
return apiRequest<DashboardDevicesData>(`/sites/${siteId}/dashboard/devices${buildQuery({ startDate, endDate, limit, filters })}`)
|
||||
}
|
||||
|
||||
export function getPublicDashboardDevices(
|
||||
@@ -346,8 +348,8 @@ export function getPublicDashboardDevices(
|
||||
return apiRequest<DashboardDevicesData>(`/public/sites/${siteId}/dashboard/devices${buildQuery({ startDate, endDate, limit }, { password, captcha })}`)
|
||||
}
|
||||
|
||||
export function getDashboardReferrers(siteId: string, startDate?: string, endDate?: string, limit = 10): Promise<DashboardReferrersData> {
|
||||
return apiRequest<DashboardReferrersData>(`/sites/${siteId}/dashboard/referrers${buildQuery({ startDate, endDate, limit })}`)
|
||||
export function getDashboardReferrers(siteId: string, startDate?: string, endDate?: string, limit = 10, filters?: string): Promise<DashboardReferrersData> {
|
||||
return apiRequest<DashboardReferrersData>(`/sites/${siteId}/dashboard/referrers${buildQuery({ startDate, endDate, limit, filters })}`)
|
||||
}
|
||||
|
||||
export function getPublicDashboardReferrers(
|
||||
@@ -357,8 +359,8 @@ export function getPublicDashboardReferrers(
|
||||
return apiRequest<DashboardReferrersData>(`/public/sites/${siteId}/dashboard/referrers${buildQuery({ startDate, endDate, limit }, { password, captcha })}`)
|
||||
}
|
||||
|
||||
export function getDashboardPerformance(siteId: string, startDate?: string, endDate?: string): Promise<DashboardPerformanceData> {
|
||||
return apiRequest<DashboardPerformanceData>(`/sites/${siteId}/dashboard/performance${buildQuery({ startDate, endDate })}`)
|
||||
export function getDashboardPerformance(siteId: string, startDate?: string, endDate?: string, filters?: string): Promise<DashboardPerformanceData> {
|
||||
return apiRequest<DashboardPerformanceData>(`/sites/${siteId}/dashboard/performance${buildQuery({ startDate, endDate, filters })}`)
|
||||
}
|
||||
|
||||
export function getPublicDashboardPerformance(
|
||||
@@ -368,8 +370,8 @@ export function getPublicDashboardPerformance(
|
||||
return apiRequest<DashboardPerformanceData>(`/public/sites/${siteId}/dashboard/performance${buildQuery({ startDate, endDate }, { password, captcha })}`)
|
||||
}
|
||||
|
||||
export function getDashboardGoals(siteId: string, startDate?: string, endDate?: string, limit = 10): Promise<DashboardGoalsData> {
|
||||
return apiRequest<DashboardGoalsData>(`/sites/${siteId}/dashboard/goals${buildQuery({ startDate, endDate, limit })}`)
|
||||
export function getDashboardGoals(siteId: string, startDate?: string, endDate?: string, limit = 10, filters?: string): Promise<DashboardGoalsData> {
|
||||
return apiRequest<DashboardGoalsData>(`/sites/${siteId}/dashboard/goals${buildQuery({ startDate, endDate, limit, filters })}`)
|
||||
}
|
||||
|
||||
export function getPublicDashboardGoals(
|
||||
@@ -378,3 +380,25 @@ export function getPublicDashboardGoals(
|
||||
): Promise<DashboardGoalsData> {
|
||||
return apiRequest<DashboardGoalsData>(`/public/sites/${siteId}/dashboard/goals${buildQuery({ startDate, endDate, limit }, { password, captcha })}`)
|
||||
}
|
||||
|
||||
// ─── Event Properties ────────────────────────────────────────────────
|
||||
|
||||
export interface EventPropertyKey {
|
||||
key: string
|
||||
count: number
|
||||
}
|
||||
|
||||
export interface EventPropertyValue {
|
||||
value: string
|
||||
count: number
|
||||
}
|
||||
|
||||
export function getEventPropertyKeys(siteId: string, eventName: string, startDate?: string, endDate?: string): Promise<EventPropertyKey[]> {
|
||||
return apiRequest<{ keys: EventPropertyKey[] }>(`/sites/${siteId}/goals/${encodeURIComponent(eventName)}/properties${buildQuery({ startDate, endDate })}`)
|
||||
.then(r => r?.keys || [])
|
||||
}
|
||||
|
||||
export function getEventPropertyValues(siteId: string, eventName: string, propName: string, startDate?: string, endDate?: string, limit = 20): Promise<EventPropertyValue[]> {
|
||||
return apiRequest<{ values: EventPropertyValue[] }>(`/sites/${siteId}/goals/${encodeURIComponent(eventName)}/properties/${encodeURIComponent(propName)}${buildQuery({ startDate, endDate, limit })}`)
|
||||
.then(r => r?.values || [])
|
||||
}
|
||||
|
||||
60
lib/filters.ts
Normal file
60
lib/filters.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
// * Dimension filter types and utilities for dashboard filtering
|
||||
|
||||
export interface DimensionFilter {
|
||||
dimension: string
|
||||
operator: 'is' | 'is_not' | 'contains' | 'not_contains'
|
||||
values: string[]
|
||||
}
|
||||
|
||||
export const DIMENSION_LABELS: Record<string, string> = {
|
||||
page: 'Page',
|
||||
referrer: 'Referrer',
|
||||
country: 'Country',
|
||||
city: 'City',
|
||||
region: 'Region',
|
||||
browser: 'Browser',
|
||||
os: 'OS',
|
||||
device: 'Device',
|
||||
utm_source: 'UTM Source',
|
||||
utm_medium: 'UTM Medium',
|
||||
utm_campaign: 'UTM Campaign',
|
||||
}
|
||||
|
||||
export const OPERATOR_LABELS: Record<string, string> = {
|
||||
is: 'is',
|
||||
is_not: 'is not',
|
||||
contains: 'contains',
|
||||
not_contains: 'does not contain',
|
||||
}
|
||||
|
||||
export const DIMENSIONS = Object.keys(DIMENSION_LABELS)
|
||||
export const OPERATORS = Object.keys(OPERATOR_LABELS) as DimensionFilter['operator'][]
|
||||
|
||||
/** Serialize filters to query param format: "browser|is|Chrome,country|is|US" */
|
||||
export function serializeFilters(filters: DimensionFilter[]): string {
|
||||
if (!filters.length) return ''
|
||||
return filters
|
||||
.map(f => `${f.dimension}|${f.operator}|${f.values.join(';')}`)
|
||||
.join(',')
|
||||
}
|
||||
|
||||
/** Parse filters from URL search param string */
|
||||
export function parseFiltersFromURL(raw: string): DimensionFilter[] {
|
||||
if (!raw) return []
|
||||
return raw.split(',').map(part => {
|
||||
const [dimension, operator, valuesRaw] = part.split('|')
|
||||
return {
|
||||
dimension,
|
||||
operator: operator as DimensionFilter['operator'],
|
||||
values: valuesRaw?.split(';') ?? [],
|
||||
}
|
||||
}).filter(f => f.dimension && f.operator && f.values.length > 0)
|
||||
}
|
||||
|
||||
/** Build display label for a filter pill */
|
||||
export function filterLabel(f: DimensionFilter): string {
|
||||
const dim = DIMENSION_LABELS[f.dimension] || f.dimension
|
||||
const op = OPERATOR_LABELS[f.operator] || f.operator
|
||||
const val = f.values.length > 1 ? `${f.values[0]} +${f.values.length - 1}` : f.values[0]
|
||||
return `${dim} ${op} ${val}`
|
||||
}
|
||||
@@ -35,14 +35,14 @@ import type {
|
||||
const fetchers = {
|
||||
site: (siteId: string) => getSite(siteId),
|
||||
dashboard: (siteId: string, start: string, end: string) => getDashboard(siteId, start, end),
|
||||
dashboardOverview: (siteId: string, start: string, end: string, interval?: string) => getDashboardOverview(siteId, start, end, interval),
|
||||
dashboardPages: (siteId: string, start: string, end: string) => getDashboardPages(siteId, start, end),
|
||||
dashboardLocations: (siteId: string, start: string, end: string) => getDashboardLocations(siteId, start, end),
|
||||
dashboardDevices: (siteId: string, start: string, end: string) => getDashboardDevices(siteId, start, end),
|
||||
dashboardReferrers: (siteId: string, start: string, end: string) => getDashboardReferrers(siteId, start, end),
|
||||
dashboardPerformance: (siteId: string, start: string, end: string) => getDashboardPerformance(siteId, start, end),
|
||||
dashboardGoals: (siteId: string, start: string, end: string) => getDashboardGoals(siteId, start, end),
|
||||
stats: (siteId: string, start: string, end: string) => getStats(siteId, start, end),
|
||||
dashboardOverview: (siteId: string, start: string, end: string, interval?: string, filters?: string) => getDashboardOverview(siteId, start, end, interval, filters),
|
||||
dashboardPages: (siteId: string, start: string, end: string, filters?: string) => getDashboardPages(siteId, start, end, undefined, filters),
|
||||
dashboardLocations: (siteId: string, start: string, end: string, filters?: string) => getDashboardLocations(siteId, start, end, undefined, undefined, filters),
|
||||
dashboardDevices: (siteId: string, start: string, end: string, filters?: string) => getDashboardDevices(siteId, start, end, undefined, filters),
|
||||
dashboardReferrers: (siteId: string, start: string, end: string, filters?: string) => getDashboardReferrers(siteId, start, end, undefined, filters),
|
||||
dashboardPerformance: (siteId: string, start: string, end: string, filters?: string) => getDashboardPerformance(siteId, start, end, filters),
|
||||
dashboardGoals: (siteId: string, start: string, end: string, filters?: string) => getDashboardGoals(siteId, start, end, undefined, filters),
|
||||
stats: (siteId: string, start: string, end: string, filters?: string) => getStats(siteId, start, end, filters),
|
||||
dailyStats: (siteId: string, start: string, end: string, interval: 'hour' | 'day' | 'minute') =>
|
||||
getDailyStats(siteId, start, end, interval),
|
||||
realtime: (siteId: string) => getRealtime(siteId),
|
||||
@@ -94,10 +94,10 @@ export function useDashboard(siteId: string, start: string, end: string) {
|
||||
}
|
||||
|
||||
// * Hook for stats (refreshed less frequently)
|
||||
export function useStats(siteId: string, start: string, end: string) {
|
||||
export function useStats(siteId: string, start: string, end: string, filters?: string) {
|
||||
return useSWR<Stats>(
|
||||
siteId && start && end ? ['stats', siteId, start, end] : null,
|
||||
() => fetchers.stats(siteId, start, end),
|
||||
siteId && start && end ? ['stats', siteId, start, end, filters] : null,
|
||||
() => fetchers.stats(siteId, start, end, filters),
|
||||
{
|
||||
...dashboardSWRConfig,
|
||||
// * Refresh every 60 seconds for stats
|
||||
@@ -144,10 +144,10 @@ export function useRealtime(siteId: string, refreshInterval: number = 5000) {
|
||||
}
|
||||
|
||||
// * Hook for focused dashboard overview data (Fix 4.2: Efficient Data Transfer)
|
||||
export function useDashboardOverview(siteId: string, start: string, end: string, interval?: string) {
|
||||
export function useDashboardOverview(siteId: string, start: string, end: string, interval?: string, filters?: string) {
|
||||
return useSWR<DashboardOverviewData>(
|
||||
siteId && start && end ? ['dashboardOverview', siteId, start, end, interval] : null,
|
||||
() => fetchers.dashboardOverview(siteId, start, end, interval),
|
||||
siteId && start && end ? ['dashboardOverview', siteId, start, end, interval, filters] : null,
|
||||
() => fetchers.dashboardOverview(siteId, start, end, interval, filters),
|
||||
{
|
||||
...dashboardSWRConfig,
|
||||
refreshInterval: 60 * 1000,
|
||||
@@ -157,10 +157,10 @@ export function useDashboardOverview(siteId: string, start: string, end: string,
|
||||
}
|
||||
|
||||
// * Hook for focused dashboard pages data
|
||||
export function useDashboardPages(siteId: string, start: string, end: string) {
|
||||
export function useDashboardPages(siteId: string, start: string, end: string, filters?: string) {
|
||||
return useSWR<DashboardPagesData>(
|
||||
siteId && start && end ? ['dashboardPages', siteId, start, end] : null,
|
||||
() => fetchers.dashboardPages(siteId, start, end),
|
||||
siteId && start && end ? ['dashboardPages', siteId, start, end, filters] : null,
|
||||
() => fetchers.dashboardPages(siteId, start, end, filters),
|
||||
{
|
||||
...dashboardSWRConfig,
|
||||
refreshInterval: 60 * 1000,
|
||||
@@ -170,10 +170,10 @@ export function useDashboardPages(siteId: string, start: string, end: string) {
|
||||
}
|
||||
|
||||
// * Hook for focused dashboard locations data
|
||||
export function useDashboardLocations(siteId: string, start: string, end: string) {
|
||||
export function useDashboardLocations(siteId: string, start: string, end: string, filters?: string) {
|
||||
return useSWR<DashboardLocationsData>(
|
||||
siteId && start && end ? ['dashboardLocations', siteId, start, end] : null,
|
||||
() => fetchers.dashboardLocations(siteId, start, end),
|
||||
siteId && start && end ? ['dashboardLocations', siteId, start, end, filters] : null,
|
||||
() => fetchers.dashboardLocations(siteId, start, end, filters),
|
||||
{
|
||||
...dashboardSWRConfig,
|
||||
refreshInterval: 60 * 1000,
|
||||
@@ -183,10 +183,10 @@ export function useDashboardLocations(siteId: string, start: string, end: string
|
||||
}
|
||||
|
||||
// * Hook for focused dashboard devices data
|
||||
export function useDashboardDevices(siteId: string, start: string, end: string) {
|
||||
export function useDashboardDevices(siteId: string, start: string, end: string, filters?: string) {
|
||||
return useSWR<DashboardDevicesData>(
|
||||
siteId && start && end ? ['dashboardDevices', siteId, start, end] : null,
|
||||
() => fetchers.dashboardDevices(siteId, start, end),
|
||||
siteId && start && end ? ['dashboardDevices', siteId, start, end, filters] : null,
|
||||
() => fetchers.dashboardDevices(siteId, start, end, filters),
|
||||
{
|
||||
...dashboardSWRConfig,
|
||||
refreshInterval: 60 * 1000,
|
||||
@@ -196,10 +196,10 @@ export function useDashboardDevices(siteId: string, start: string, end: string)
|
||||
}
|
||||
|
||||
// * Hook for focused dashboard referrers data
|
||||
export function useDashboardReferrers(siteId: string, start: string, end: string) {
|
||||
export function useDashboardReferrers(siteId: string, start: string, end: string, filters?: string) {
|
||||
return useSWR<DashboardReferrersData>(
|
||||
siteId && start && end ? ['dashboardReferrers', siteId, start, end] : null,
|
||||
() => fetchers.dashboardReferrers(siteId, start, end),
|
||||
siteId && start && end ? ['dashboardReferrers', siteId, start, end, filters] : null,
|
||||
() => fetchers.dashboardReferrers(siteId, start, end, filters),
|
||||
{
|
||||
...dashboardSWRConfig,
|
||||
refreshInterval: 60 * 1000,
|
||||
@@ -209,10 +209,10 @@ export function useDashboardReferrers(siteId: string, start: string, end: string
|
||||
}
|
||||
|
||||
// * Hook for focused dashboard performance data
|
||||
export function useDashboardPerformance(siteId: string, start: string, end: string) {
|
||||
export function useDashboardPerformance(siteId: string, start: string, end: string, filters?: string) {
|
||||
return useSWR<DashboardPerformanceData>(
|
||||
siteId && start && end ? ['dashboardPerformance', siteId, start, end] : null,
|
||||
() => fetchers.dashboardPerformance(siteId, start, end),
|
||||
siteId && start && end ? ['dashboardPerformance', siteId, start, end, filters] : null,
|
||||
() => fetchers.dashboardPerformance(siteId, start, end, filters),
|
||||
{
|
||||
...dashboardSWRConfig,
|
||||
refreshInterval: 60 * 1000,
|
||||
@@ -222,10 +222,10 @@ export function useDashboardPerformance(siteId: string, start: string, end: stri
|
||||
}
|
||||
|
||||
// * Hook for focused dashboard goals data
|
||||
export function useDashboardGoals(siteId: string, start: string, end: string) {
|
||||
export function useDashboardGoals(siteId: string, start: string, end: string, filters?: string) {
|
||||
return useSWR<DashboardGoalsData>(
|
||||
siteId && start && end ? ['dashboardGoals', siteId, start, end] : null,
|
||||
() => fetchers.dashboardGoals(siteId, start, end),
|
||||
siteId && start && end ? ['dashboardGoals', siteId, start, end, filters] : null,
|
||||
() => fetchers.dashboardGoals(siteId, start, end, filters),
|
||||
{
|
||||
...dashboardSWRConfig,
|
||||
refreshInterval: 60 * 1000,
|
||||
|
||||
Reference in New Issue
Block a user