Мы в компании используем Cube (cube.dev) уже пару лет. Я с ним знаком гораздо дольше, но раньше понятия не имел, зачем он 😅. А когда пришлось эволюционировать из дашбордов, то вспомнил про него, и с тех пор всем рекомендую. В эпоху ИИ это вообще незаменимая вещь.
Cube позволяет описывать данные для BI, обозначая, какое поле в таблице чем является, как агрегируется, с чем джойнится и т.п. Помимо базовых атрибутов у полей и у самих кубов имеется свободный признак meta для передачи потребителю чего угодно.
Мы с командой для любимого клиента 💚 активно развиваем веб-интерфейс интерактивной отчетности по маркетинговым активностям, используем для этого Cube и наскребли по нему как минимум на жёлтый пояс, так что делюсь некоторыми техниками.
Общие настройки отчета
В meta атрибуте самого куба мы передаём шорткаты по периодам. К примеру, есть отчеты без дат, есть с подневной разбивкой, понедельной, помесячной, свежими данными и не очень.
В веб интерфейсе пользователю показываются эти самые шорткаты.
А на стороне Cube это выглядит примерно так.
meta: {
recommendedPeriods: {
last: [
{ title: 'Вчера', range: 'Yesterday', },
{ title: 'Неделя', range: 'last 7 days' },
...
]
}
},
Справочная информация
При подготовке данных запускается огромное количество процессов, связанных с DQ (Darko Quality), и даже подсвечиваются предполагаемые проблемы в финальных отчетах.
Тесты выполняются типовые, комментарии для них тоже типовые, и как-то нерентабельно передавать одни и те же описания на каждую строчку, поэтому мы решили в данных отдавать "номер ошибки", а в meta куба держать описание этих ошибок.
И да, этот пример внезапно на мямле.
## DQ Cube
meta:
statuses:
meta:
1: Замечена проблема с размещением, проверьте разметку.
2: Подозрительно низкий показатель конверсий.
3: Эти данные взяты из ненадежного источника.
В целевом кубе, к которому цепляется DQ куб, тоже используется это описание. Ссылаемся на него через type: joinedMeta.
// target cube
dq_status: {
sql: `${DQ.dq_status}`,
type: `string`,
title: `DQ статус`,
meta: {
type: 'joinedMeta',
cubeName: 'DQ',
},
},
Описание для ИИ (MCP)
Справки требуются не только нам, но и братьям нашим меньшим. Аналитики дают пояснения полям, которые помогают нейросетям "понять" датасет и вернуть нужные данные пользователю. Мы почему-то решили писать на английском, чтобы сэкономить токены, но эффективность клинически не доказана.
cpa: {
sql: `${CUBE.cost}/${CUBE.conversions}`
type: `number`,
title: `CPA`,
meta: {
ai_description:
'This is the actual cost per action according to data from advertising platform. Lower CPA means better performance of placement. But if CPA is low and number of conversions is also low, placement is not effective, and settings may be corrected to improve performance.',
},
format: `currency`,
},
Группы полей
База для любого конструктора отчетности - группировка полей по категориям. У нас это может быть разделение фактических и плановых данных по рекламе, разделение конверсий по системам, post-click, post-view, post-bullshit показателей, и вообще как угодно.
Добавляем простой атрибут с названием группы.
budget: {
sql: `${CUBE}.budget`,
type: 'sum',
title: 'Бюджет',
meta: {
group: 'План',
}
},
cost: {
sql: `${CUBE}.cost`,
type: 'sum',
title: 'Затраты',
meta: {
group: 'Факт',
}
},
Тотал из другого поля
Хорошего менеджера от плохого отличает строчка "Итого", поэтому в наших отчетах она всегда присутствует. 😁
Однако в маркетинговых расчетах бывает такое, что это самое "Итого" считается не обычной агрегирующей функцией, а отдельным алгоритмом. В этом случае мы указываем в meta, какое поле нужно поставить в total. Если пользователь выбирает такую замысловатую метрику, фронт при формировании запроса в Cube автоматически добавляет дополнительные нужные для расчета поля.
vtr: {
meta: {
total: 'vtr_total'
},
}
Расчетная
Если в отчете есть план, то юзер обязательно должен иметь возможность наблюдать его выполнение. Мы ввели расчетный тип поля и передаём к нему аргументы. На фронте поля, указанные в расчетах, запрашивается дополнительно, подставляются в формулу и красиво выводятся.
cost: {
sql: `cost`,
type: `sum`,
title: `Расходы`,
meta: {
type: 'calculated',
args: ['plan_cost'],
},
}
Ограничение выбора
Не все поля можно выбирать пользователям. Вообще Cube позволяет помечать сущность public::boolean, но это влияет на их видимость для запросов через api, а мы столкнулись со случаями, когда поле нам все-таки нужно для подкапотного использования. Решили запрещать юзерам выбирать такое поле собственным признаком selectable, и оно стало влиять на видимость в селекторе.
meta: {
selectable: false,
}
Стилизация
Атрибут meta - самое место для стилизации. Просто так смотреть цифры скучно, и не всегда удобно. Можно добавить в табличку яркие элементы
Чипсы
Например, статусы по традиции мы обернули в чипсы. Прикольно же!
meta: {
group: 'Кампания',
type: 'chips',
variants: {
Завершена: '#cdc1ff',
Активна: '#def7d8',
Готовится: '#fed277',
Проблема: '#ffe3e3',
},
},
Светофор
Вдохновившись своими же наработками по выполнению плана, мы решили дать возможность свободно раскрашивать процентные значения. В таких случаях наш куб передаёт цвет и границы раскраски: перебздели или недобздели - плохо.
meta: {
type: 'rainbow',
rules: {
'#FF820F': [
[null, 90],
[110, null],
],
'#7FE066': [[90, 110]],
},
},
Прочее
Как сортировать, как округлять, как схлопывать - всё здесь.
meta: {
order: 'asc', // рекомендуемая сортировка
type: 'week', // тип даты
precision: 2, // округления
}
Ссылка на другой отчет
Венец творения - иерархия отчетов. Смотрим список рекламных кампаний, и хотим узнать по какойто из них подробности. Но эти подробности невозможно приклеить ко многим поля общего отчета, поэтому они живут отдельно. Наш куб говорит фронтенду, что поле "Название кампании" является ссылкой и передаёт полный перечень настроек отчета, на которые пользователь попадёт, ткнув по ней.
name: {
sql: `${CUBE}."Name"`,
type: `string`,
title: `Название кампании`,
meta: {
reportLink: true,
filtersByCubeFields: [
{
source: 'campaigns.id',
operator: 'равно',
target: 'campaign_details.id',
},
],
template: {
cubeName: 'campaign_details',
dimensions: [
'campaign_details.name',
'campaign_details.utm_campaign',
'campaign_details.utm_term'
]
measures: [
'campaign_details.impressions',
'campaign_details.clicks',
'campaign_details.cost',
'campaign_details.cpc',
'campaign_details.ctr'
],
},
},
},