Paywall или AdSense? Как мы динамически скрывали рекламу от «китов» с помощью Firebase и Google Analytics
Конфликт интересов: Жадность продакта против здравого смысла
В мире мобильных и веб-приложений есть два стула. На одном — AdSense (или любая другая рекламная сетка): стабильные, но копеечные доходы от показов баннеров, видео с вознаграждением и раздражающих полноэкранных interstitial-ов. На другом — Paywall (подписки/In-App Purchases): золотая жила, где пользователи платят реальные деньги за премиум-функции или отключение рекламы.
Любой продакт-менеджер рано или поздно сталкивается с дилеммой. С одной стороны, мы хотим показывать как можно больше рекламы тем 95% пользователей, которые никогда нам не заплатят (так называемым «халявщикам» или «free-riders»). С другой стороны, у нас есть оставшиеся 5% — это наши «киты» (whales). Это люди, которые готовы занести нам $50, $100 или оформить годовую подписку.
И вот тут начинается боль. Если мы покажем нашему потенциальному «киту» три полноэкранных рекламных ролика про казино подряд, он просто удалит приложение, пробормотав проклятия в адрес разработчиков. Мы потеряем $50 ради того, чтобы заработать $0.02 на рекламе. Это экономический суицид.
Так родилась задача: как нам в реальном времени понять, что перед нами «кит» (или потенциальный кит), и динамически скрыть от него всю рекламу, не дожидаясь, пока он сам купит премиум? Как сделать ему максимально гладкий, элитный UX, чтобы он влюбился в продукт и сам нажал на кнопку «Купить»?
В этой статье (которая по объему тянет на небольшую диссертацию) я расскажу, как команда ОЗАТ построила систему динамического управления рекламной нагрузкой на базе Google Analytics 4 (GA4), BigQuery, машинного обучения (Vertex AI) и Firebase. Готовьтесь, будет много инженерного мяса, кэширования и борьбы за миллисекунды.
Анатомия «Кита»: Кого мы ищем?
Прежде чем писать код, давайте определимся, кого мы прячем от рекламы. В нашем случае мы разделили аудиторию на три сегмента:
- Confirmed Whales (Подтвержденные киты): Уже купили премиум или совершили In-App Purchase. Тут все просто — рекламу отключаем по дефолту. Это базовый функционал.
- High Propensity to Buy (Высокая вероятность покупки): Эти ребята еще ничего не купили, но их поведение кричит: «Я готов!». Они заходят в приложение каждый день, активно пользуются core-фичами, часто заходят на экран Paywall, но почему-то не конвертятся. Возможно, их раздражает реклама. Если мы ее уберем, мы повысим их лояльность.
- Free-riders (Обычные пользователи): Заходят раз в месяц, кликают пару кнопок, уходят. Вероятность покупки — 0.01%. Вот на них мы и будем откручивать наши рекламные баннеры на полную катушку.
Самая сложная категория — вторая. Как вычислить High Propensity в реальном времени? На помощь приходит ML.
Data Pipeline: От клика до скоринга
Архитектура нашего решения выглядит следующим образом:
App -> GA4 -> BigQuery -> Vertex AI (ML Model) -> Cloud Functions -> Firebase (Custom Claims / Firestore) -> AppДавайте разберем каждый шаг подробно.
Шаг 1: GA4 и BigQuery (Сбор сырых данных)
Все события из приложения (клики, просмотры экранов, сессии) сыплются в Firebase/GA4. Мы настраиваем ежедневный экспорт сырых данных (Daily Export) из GA4 в BigQuery. В BigQuery мы получаем те самые таблицы events_YYYYMMDD.
Но сырые данные для ML-модели не годятся. Нам нужен Feature Engineering. Мы написали dbt-модели, которые каждый день агрегируют поведение пользователей (по user_pseudo_id или user_id) за последние 14 дней.
Какие фичи мы собираем:
- RFM (Recency, Frequency, Monetary): Сколько дней назад был последний заход, сколько сессий всего, сколько потрачено (пока 0).
- Engagement: Средняя длительность сессии, количество экранов за сессию.
- Paywall Intents: Сколько раз пользователь открывал экран покупки.
- Ad Tolerance: Сколько рекламных роликов он посмотрел до конца, а сколько скипнул.
Шаг 2: Машинное обучение в Vertex AI
На агрегированных данных мы обучаем модель классификации (например, XGBoost). Таргет (Y) — совершил ли пользователь покупку в следующие 7 дней (1 или 0).
Мы используем Vertex AI AutoML или кастомные пайплайны на Python. Модель тренируется раз в неделю. А вот Batch Prediction (скоринг всех текущих активных пользователей) мы запускаем каждую ночь.
Результатом работы Vertex AI является таблица в BigQuery вида:
user_id | propensity_score | segment
------------------------------------
user_A | 0.89 | High
user_B | 0.05 | Low
user_C | 0.45 | MediumШаг 3: Синхронизация с Firebase (The Tricky Part)
Окей, у нас есть таблица в BigQuery с сегментами пользователей. Как теперь приложению узнать об этом без задержек? Делать SQL-запрос из мобильного приложения в BigQuery? Боже упаси! Это медленно, дорого и небезопасно.
Нам нужно прокинуть этот статус на клиент. Идеальное место для хранения такого стейта — Firebase. Но какой именно сервис Firebase выбрать?
У нас есть три варианта:
- Firestore (База данных): Хранить документ
users/{userId}с полемsegment: "High". - Firebase Remote Config: Использовать условия (Conditions) по Google Analytics Audiences.
- Firebase Auth Custom Claims: Зашить статус прямо в JWT-токен пользователя.
Почему не Firestore?
Firestore — отличный выбор. Мобилка подписывается на документ (Snapshot Listener). Cloud Function каждую ночь читает таблицу из BigQuery и делает батч-обновления (batch writes) в Firestore.
Минус: Дополнительные чтения (reads) из базы данных = дополнительные косты. Плюс, нам нужно дождаться инициализации SDK и первого ответа от базы, что может занять 300-500мс при холодном старте. Это приведет к эффекту "Flickering" (мерцания рекламы).
Как мы перевезли крупный проект на WordPress в Google Cloud и не поседели
Почему не Remote Config + GA Audiences?
Кажется, это самый "no-code" путь. Создаем аудиторию в GA4, ждем, пока она доедет до Remote Config, и меняем параметр show_ads = false.
Минус: Задержка синхронизации. Аудитории из GA4 доезжают до Firebase до 24 часов. Это слишком медленно. Мы потеряем "тепленького" пользователя.
Наш выбор: Firebase Auth Custom Claims
Custom Claims (пользовательские утверждения) — это магия. Вы можете добавить любую кастомную информацию прямо в авторизационный токен пользователя (JWT). Когда пользователь открывает приложение, токен уже лежит в локальном кэше устройства. Клиенту вообще не нужно делать сетевых запросов, чтобы узнать свой сегмент!
// Cloud Function: Синхронизация BigQuery -> Firebase Auth
import * as admin from 'firebase-admin';
import { BigQuery } from '@google-cloud/bigquery';
const bq = new BigQuery();
exports.syncWhalesToAuth = functions.pubsub.schedule('every 24 hours').onRun(async (context) => {
const query = `SELECT user_id, segment FROM `my_project.marts.user_segments` WHERE segment = 'High'`;
const [rows] = await bq.query(query);
for (const row of rows) {
// Устанавливаем Custom Claim 'is_whale' = true
await admin.auth().setCustomUserClaims(row.user_id, { is_whale: true });
}
console.log(`Синхронизировано ${rows.length} китов.`);
});Борьба за миллисекунды: Клиентская реализация (React)
Если мы будем рендерить рекламный баннер, а потом через полсекунды его скрывать (потому что до нас долетел статус, что это кит) — это будет выглядеть ужасно. Верстка прыгнет (Cumulative Layout Shift), пользователь случайно кликнет по пустому месту (или, что хуже, по рекламе, которую мы хотели скрыть).
Используя Firebase Auth, мы можем синхронно проверить токен на старте приложения.
// React: Проверка статуса пользователя
import { useEffect, useState } from 'react';
import { getAuth, onAuthStateChanged, getIdTokenResult } from 'firebase/auth';
export const useAdStrategy = () => {
const [showAds, setShowAds] = useState<boolean | null>(null); // null = загрузка
useEffect(() => {
const auth = getAuth();
const unsubscribe = onAuthStateChanged(auth, async (user) => {
if (user) {
// Получаем claims. ForceRefresh = false для скорости!
const tokenResult = await getIdTokenResult(user, false);
const isWhale = tokenResult.claims.is_whale === true;
setShowAds(!isWhale);
} else {
// Незалогиненным - показываем рекламу на всю катушку
setShowAds(true);
}
});
return () => unsubscribe();
}, []);
return showAds;
};Антипаттерн: "Пусть пока повисит, а там разберемся"
Никогда не используйте showAds: true по умолчанию, пока идет проверка. Лучше покажите лоадер или пустой skeleton (скелетон) рекламного блока на 100мс, чем заставлять интерфейс прыгать. Значение по умолчанию должно быть null или "pending state".
Хитрый трюк: Динамический Paywall (Soft Paywall)
Окей, мы скрыли рекламу от «китов». Что дальше? Радоваться, что мы не бесим людей?
Нет, продакт-менеджер все еще хочет денег. Если мы просто уберем рекламу, «кит» может вообще забыть, что у нас есть платная подписка. Ему и так хорошо!
Поэтому мы заменили рекламные блоки на Soft Paywall Banners. Вместо рекламы казино от AdSense, мы в тех же местах аккуратно и нативно показываем баннеры наших же премиум-фич.
«Вам нравится наше приложение без рекламы? Оформите Premium сейчас со скидкой 50%, чтобы открыть аналитику и экспорт отчетов!»
По сути, мы превратили рекламный инвентарь в собственный инструмент апселла (Upsell).
A/B Тестирование и Аналитика: Цифры не врут
Конечно, мы не могли выкатить эту логику на 100% пользователей без проверки. Мы запустили А/В тест через Firebase A/B Testing, разделив аудиторию (группу High Propensity) пополам.
- Контрольная группа (Группа А): Продолжала получать обычную рекламу AdSense.
- Тестовая группа (Группа В): Реклама была отключена (заменена на Soft Paywall).
Шокирующие (и не очень) результаты:
- Ad Revenue (Рекламные доходы) в группе В логично упали на 12%. Это те самые копейки, которые мы недополучили от показов баннеров китам.
- Conversion Rate to Premium (Конверсия в подписку) в группе В ВЫРОСЛА НА 47%. Оказалось, что чистый UX и нативный апселл работают в разы эффективнее, чем агрессивное принуждение через раздражение.
- Retention Rate (Удержание 7-го дня) среди High Propensity пользователей увеличился на 15%. Они перестали удалять приложение от бешенства.
Результаты А/В теста: AdSense vs Soft Paywall (для High Propensity сегмента)
Сравнение ключевых бизнес-метрик при полном отключении стандартной рекламы и замене ее на внутренний апселл.
Масштабирование: Real-time ML с Vertex AI Endpoint
Описанный выше пайплайн работает в пакетном режиме (Batch). Мы пересчитываем сегменты ночью. Этого достаточно для 90% приложений.
Но что, если пользователь стал "китом" в течение одной сессии? Например, он только что привязал кредитную карту или добавил 10 товаров в корзину. Мы хотим убрать рекламу не завтра утром, а прямо СЕЙЧАС.
Для этого мы развернули модель как Vertex AI Endpoint (API реального времени). Когда пользователь совершает ключевое действие (Key Event), мобильное приложение отправляет HTTP-запрос на нашу Cloud Function. Функция дергает Endpoint, получает свежий скор, и если скор > 0.9, тут же делает admin.auth().setCustomUserClaims(...) и просит клиент форсированно обновить токен (getIdToken(true)).
Реклама пропадает буквально на глазах, прямо во время сессии. Это производит вау-эффект.
Заключение: Инвестируйте в Data-архитектуру
Бизнес часто смотрит на монетизацию как на рубильник: "Давай включим рекламу всем, кому можно". В краткосрочной перспективе это приносит деньги. В долгосрочной — выжигает лояльную аудиторию и убивает LTV (Lifetime Value).
С помощью связки Google Analytics 4, BigQuery, ML-моделей в Vertex AI и мгновенной доставки стейта через Firebase Auth Custom Claims, мы превратили монетизацию из топора в скальпель. Мы выигрываем в LTV, улучшаем UX и делаем счастливее и пользователей, и финансового директора.
Внедрение такой системы требует инженерной культуры и тесного взаимодействия между Data Science и Frontend-разработчиками. Но как только вы увидите графики роста конверсии, вы поймете — игра стоила свеч.
А как вы балансируете между рекламой и подписками в ваших продуктах? Делитесь болью в комментариях (или пишите нам в ОЗАТ, мы знаем, как это лечить).

Рустам Шарафутдинов
Эксперт в области архитектуры Google Cloud и Senior Full-Stack разработчик с более чем 15-летним опытом. Специализируется на отказоустойчивых архитектурах, оптимизации высоконагруженных проектов и интеграции AI (Vertex AI).