Рецепты
Готовые скрипты под частые задачи байера. Каждый собран из методов, описанных на соседних страницах, - скопируйте, впишите свои значения в начале скрипта и запускайте.
Во всех рецептах замените ВАШ_КЛЮЧ и panel.example.com на свои. Как получить ключ, см. раздел «Введение в API».
Даты - по UTC
Статистика и конверсии считаются по дням UTC. «Вчера» в скриптах ниже - это вчера по UTC, а не по вашему часовому поясу.
Статистика за вчера в Telegram
Каждое утро присылает в Telegram итоги за вчера: визиты, установки, регистрации, депозиты, выплаты и конверсию.
Что понадобится:
- Свой бот: напишите @BotFather команду
/newbotи скопируйте токен. - ID чата, куда слать: напишите боту любое сообщение, откройте
https://api.telegram.org/bot<ТОКЕН>/getUpdatesи возьмитеchat.idиз ответа. Для группы - добавьте бота в группу, напишите в ней сообщение и возьмитеchat.idгруппы (он отрицательный).
# pip install requests
import datetime
import requests
API = "https://panel.example.com/api"
HEADERS = {"Authorization": "Bearer ВАШ_КЛЮЧ", "Accept": "application/json"}
BOT_TOKEN = "ТОКЕН_БОТА"
CHAT_ID = "ID_ЧАТА"
day = (datetime.datetime.now(datetime.timezone.utc).date() - datetime.timedelta(days=1)).isoformat()
response = requests.get(f"{API}/stats/dashboard", headers=HEADERS, params={
"date_from": day,
"date_to": day,
}, timeout=30)
response.raise_for_status()
metrics = response.json()["data"]["metrics"]
payouts = ", ".join(f'{a["amount"]} {a["currency"]}' for a in metrics["total_deposits"]["amounts"]) or "0"
text = "\n".join([
f"PWA Hub за {day}",
f"Визиты: {metrics['total_visits']['value']} ({metrics['total_visits']['trend']}%)",
f"Установки: {metrics['total_installs']['value']} ({metrics['total_installs']['trend']}%)",
f"Регистрации: {metrics['total_registrations']['value']}",
f"Депозиты: {metrics['total_deposits']['value']} - {payouts}",
f"CR: {metrics['avg_cr']['value']}%",
])
requests.post(f"https://api.telegram.org/bot{BOT_TOKEN}/sendMessage", json={
"chat_id": CHAT_ID,
"text": text,
}, timeout=30).raise_for_status()
print("Отправлено:\n" + text)// Node.js 18+, без зависимостей; сохраните как stats-telegram.mjs
const API = 'https://panel.example.com/api';
const headers = { Authorization: 'Bearer ВАШ_КЛЮЧ', Accept: 'application/json' };
const BOT_TOKEN = 'ТОКЕН_БОТА';
const CHAT_ID = 'ID_ЧАТА';
const yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000);
const day = yesterday.toISOString().slice(0, 10);
const response = await fetch(`${API}/stats/dashboard?${new URLSearchParams({ date_from: day, date_to: day })}`, {
headers,
signal: AbortSignal.timeout(30000),
});
if (!response.ok) throw new Error(`PWA Hub ответил ${response.status}`);
const { metrics } = (await response.json()).data;
const payouts = metrics.total_deposits.amounts.map((a) => `${a.amount} ${a.currency}`).join(', ') || '0';
const text = [
`PWA Hub за ${day}`,
`Визиты: ${metrics.total_visits.value} (${metrics.total_visits.trend}%)`,
`Установки: ${metrics.total_installs.value} (${metrics.total_installs.trend}%)`,
`Регистрации: ${metrics.total_registrations.value}`,
`Депозиты: ${metrics.total_deposits.value} - ${payouts}`,
`CR: ${metrics.avg_cr.value}%`,
].join('\n');
const sent = await fetch(`https://api.telegram.org/bot${BOT_TOKEN}/sendMessage`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ chat_id: CHAT_ID, text }),
signal: AbortSignal.timeout(30000),
});
if (!sent.ok) throw new Error(`Telegram ответил ${sent.status}`);
console.log(`Отправлено:\n${text}`);<?php
// composer require guzzlehttp/guzzle
require 'vendor/autoload.php';
$api = new GuzzleHttp\Client([
'base_uri' => 'https://panel.example.com/api/',
'headers' => ['Authorization' => 'Bearer ВАШ_КЛЮЧ', 'Accept' => 'application/json'],
'timeout' => 30,
]);
$botToken = 'ТОКЕН_БОТА';
$chatId = 'ID_ЧАТА';
$day = (new DateTimeImmutable('yesterday', new DateTimeZone('UTC')))->format('Y-m-d');
$response = $api->get('stats/dashboard', ['query' => ['date_from' => $day, 'date_to' => $day]]);
$metrics = json_decode($response->getBody(), true)['data']['metrics'];
$payouts = implode(', ', array_map(
fn($a) => "{$a['amount']} {$a['currency']}",
$metrics['total_deposits']['amounts']
)) ?: '0';
$text = implode("\n", [
"PWA Hub за {$day}",
"Визиты: {$metrics['total_visits']['value']} ({$metrics['total_visits']['trend']}%)",
"Установки: {$metrics['total_installs']['value']} ({$metrics['total_installs']['trend']}%)",
"Регистрации: {$metrics['total_registrations']['value']}",
"Депозиты: {$metrics['total_deposits']['value']} - {$payouts}",
"CR: {$metrics['avg_cr']['value']}%",
]);
(new GuzzleHttp\Client(['timeout' => 30]))->post("https://api.telegram.org/bot{$botToken}/sendMessage", [
'json' => ['chat_id' => $chatId, 'text' => $text],
]);
echo "Отправлено:\n{$text}" . PHP_EOL;По расписанию. На сервере с Linux добавьте строку в crontab -e - скрипт будет срабатывать каждый день в 07:00 по времени сервера:
0 7 * * * /usr/bin/python3 /home/user/stats-telegram.pyДля Node.js - /usr/bin/node /home/user/stats-telegram.mjs, для PHP - /usr/bin/php /home/user/stats-telegram.php. Запускайте не раньше 00:10 UTC: свежие события попадают в статистику с задержкой до 5 минут.
trend - изменение к предыдущему дню в процентах. Подробно о показателях - в разделе «Статистика».
Клонировать PWA, повесить домен и включить
Делает новое PWA из готового за один запуск: клонирует его, привязывает к копии домен и включает её. Удобно, когда под каждую связку нужно своё приложение со своим доменом.
Что понадобится: id PWA-образца (из списка PWA или адресной строки панели) и домен для копии.
Это реальные действия в Cloudflare
Привязка домена создаёт зону и DNS-записи в вашем аккаунте Cloudflare. Поддомен (go.example.com) привязывается, только если его корневой домен (example.com) уже добавлен в панель - иначе придёт 404. Подробности - в разделе «Домены».
# pip install requests
import requests
API = "https://panel.example.com/api"
HEADERS = {"Authorization": "Bearer ВАШ_КЛЮЧ", "Accept": "application/json"}
SOURCE_ID = "WZAX" # PWA-образец
NAME = "Promo EN - связка 12" # название копии
DOMAIN = "fit-tracker-12.example" # домен для копии
def call(method, path, **kwargs):
response = requests.request(method, f"{API}/{path}", headers=HEADERS, timeout=30, **kwargs)
if not response.ok:
raise SystemExit(f"{method} /{path}: {response.status_code} {response.json().get('message')}")
return response.json()["data"]
# Команда образца нужна для PUT (team_lead и admin передают team_id всегда)
team_id = call("GET", f"pwas/{SOURCE_ID}")["pwa"]["team"]["id"]
clone = call("POST", f"pwas/{SOURCE_ID}/clone", json={"names": [NAME]})["clones"][0]
print("Копия:", clone["id"], clone["name"])
domain = call("POST", "domains", json={"domain": DOMAIN, "pwa_id": clone["id"]})["domain"]
print("Домен:", domain["domain"])
call("PUT", f"pwas/{clone['id']}", json={"active": True, "team_id": team_id})
print("PWA включено")// Node.js 18+, без зависимостей; сохраните как clone-pwa.mjs
const API = 'https://panel.example.com/api';
const headers = { Authorization: 'Bearer ВАШ_КЛЮЧ', Accept: 'application/json', 'Content-Type': 'application/json' };
const SOURCE_ID = 'WZAX'; // PWA-образец
const NAME = 'Promo EN - связка 12'; // название копии
const DOMAIN = 'fit-tracker-12.example'; // домен для копии
async function call(method, path, body) {
const response = await fetch(`${API}/${path}`, {
method,
headers,
body: body ? JSON.stringify(body) : undefined,
signal: AbortSignal.timeout(30000),
});
const json = await response.json();
if (!response.ok) throw new Error(`${method} /${path}: ${response.status} ${json.message}`);
return json.data;
}
// Команда образца нужна для PUT (team_lead и admin передают team_id всегда)
const teamId = (await call('GET', `pwas/${SOURCE_ID}`)).pwa.team.id;
const [clone] = (await call('POST', `pwas/${SOURCE_ID}/clone`, { names: [NAME] })).clones;
console.log('Копия:', clone.id, clone.name);
const { domain } = await call('POST', 'domains', { domain: DOMAIN, pwa_id: clone.id });
console.log('Домен:', domain.domain);
await call('PUT', `pwas/${clone.id}`, { active: true, team_id: teamId });
console.log('PWA включено');<?php
// composer require guzzlehttp/guzzle
require 'vendor/autoload.php';
$api = new GuzzleHttp\Client([
'base_uri' => 'https://panel.example.com/api/',
'headers' => ['Authorization' => 'Bearer ВАШ_КЛЮЧ', 'Accept' => 'application/json'],
'timeout' => 30,
'http_errors' => false,
]);
$sourceId = 'WZAX'; // PWA-образец
$name = 'Promo EN - связка 12'; // название копии
$domainName = 'fit-tracker-12.example'; // домен для копии
function call(GuzzleHttp\Client $api, string $method, string $path, ?array $body = null): array
{
$response = $api->request($method, $path, $body === null ? [] : ['json' => $body]);
$json = json_decode($response->getBody(), true);
if ($response->getStatusCode() >= 400) {
exit("{$method} /{$path}: {$response->getStatusCode()} {$json['message']}" . PHP_EOL);
}
return $json['data'];
}
// Команда образца нужна для PUT (team_lead и admin передают team_id всегда)
$teamId = call($api, 'GET', "pwas/{$sourceId}")['pwa']['team']['id'];
$clone = call($api, 'POST', "pwas/{$sourceId}/clone", ['names' => [$name]])['clones'][0];
echo "Копия: {$clone['id']} {$clone['name']}" . PHP_EOL;
$domain = call($api, 'POST', 'domains', ['domain' => $domainName, 'pwa_id' => $clone['id']])['domain'];
echo "Домен: {$domain['domain']}" . PHP_EOL;
call($api, 'PUT', "pwas/{$clone['id']}", ['active' => true, 'team_id' => $teamId]);
echo 'PWA включено' . PHP_EOL;Копия получает настройки, картинки и аккаунты образца, но не поток фильтрации - если он нужен, задайте filter_stream_id в том же PUT (см. раздел «PWA»). Новое PWA начинает получать трафик в течение нескольких минут после включения.
Остановить PWA без депозитов
Проходит по всем включённым PWA и выключает те, у которых за последние 7 дней набралось достаточно установок, но нет ни одного депозита.
Что понадобится: решить, сколько установок считать достаточным (MIN_INSTALLS). По умолчанию скрипт работает в режиме проверки (DRY_RUN = True) и только печатает, что выключил бы. Убедитесь, что список верный, и поменяйте на False.
# pip install requests
import datetime
import time
import requests
API = "https://panel.example.com/api"
HEADERS = {"Authorization": "Bearer ВАШ_КЛЮЧ", "Accept": "application/json"}
MIN_INSTALLS = 50 # меньше установок - рано судить
DRY_RUN = True # True - только показать, False - выключать
today = datetime.datetime.now(datetime.timezone.utc).date()
date_from = (today - datetime.timedelta(days=7)).isoformat()
date_to = (today - datetime.timedelta(days=1)).isoformat()
def get(path, params=None):
response = requests.get(f"{API}/{path}", headers=HEADERS, params=params, timeout=30)
if response.status_code == 401:
raise SystemExit("Ключ не принят - проверьте его в профиле")
response.raise_for_status()
return response.json()["data"]
pwas, page = [], 1
while True:
data = get("pwas", {"status": "active", "page": page, "excludes": "stats"})
pwas += data["pwas"]
if page >= data["meta"]["last_page"]:
break
page += 1
for pwa in pwas:
totals = get("stats/funnel", {"date_from": date_from, "date_to": date_to, "pwa_id": pwa["id"]})["funnel"]["totals"]
time.sleep(0.25) # не больше 300 запросов в минуту
if totals["installs"] < MIN_INSTALLS or totals["deps"] > 0:
continue
print(f"{pwa['id']} {pwa['name']}: {totals['installs']} установок, 0 депозитов")
if not DRY_RUN:
requests.put(f"{API}/pwas/{pwa['id']}", headers=HEADERS, json={
"active": False,
"team_id": pwa["team"]["id"],
}, timeout=30).raise_for_status()
print(" выключено")// Node.js 18+, без зависимостей; сохраните как stop-pwas.mjs
const API = 'https://panel.example.com/api';
const headers = { Authorization: 'Bearer ВАШ_КЛЮЧ', Accept: 'application/json', 'Content-Type': 'application/json' };
const MIN_INSTALLS = 50; // меньше установок - рано судить
const DRY_RUN = true; // true - только показать, false - выключать
const day = (daysAgo) => new Date(Date.now() - daysAgo * 24 * 60 * 60 * 1000).toISOString().slice(0, 10);
const dateFrom = day(7);
const dateTo = day(1);
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function get(path, params = {}) {
const response = await fetch(`${API}/${path}?${new URLSearchParams(params)}`, {
headers,
signal: AbortSignal.timeout(30000),
});
if (response.status === 401) throw new Error('Ключ не принят - проверьте его в профиле');
if (!response.ok) throw new Error(`${path}: ${response.status}`);
return (await response.json()).data;
}
const pwas = [];
for (let page = 1; ; page++) {
const data = await get('pwas', { status: 'active', page, excludes: 'stats' });
pwas.push(...data.pwas);
if (page >= data.meta.last_page) break;
}
for (const pwa of pwas) {
const { totals } = (await get('stats/funnel', { date_from: dateFrom, date_to: dateTo, pwa_id: pwa.id })).funnel;
await sleep(250); // не больше 300 запросов в минуту
if (totals.installs < MIN_INSTALLS || totals.deps > 0) continue;
console.log(`${pwa.id} ${pwa.name}: ${totals.installs} установок, 0 депозитов`);
if (!DRY_RUN) {
const response = await fetch(`${API}/pwas/${pwa.id}`, {
method: 'PUT',
headers,
body: JSON.stringify({ active: false, team_id: pwa.team.id }),
signal: AbortSignal.timeout(30000),
});
if (!response.ok) throw new Error(`PUT /pwas/${pwa.id}: ${response.status}`);
console.log(' выключено');
}
}<?php
// composer require guzzlehttp/guzzle
require 'vendor/autoload.php';
$api = new GuzzleHttp\Client([
'base_uri' => 'https://panel.example.com/api/',
'headers' => ['Authorization' => 'Bearer ВАШ_КЛЮЧ', 'Accept' => 'application/json'],
'timeout' => 30,
]);
$minInstalls = 50; // меньше установок - рано судить
$dryRun = true; // true - только показать, false - выключать
$today = new DateTimeImmutable('today', new DateTimeZone('UTC'));
$dateFrom = $today->modify('-7 days')->format('Y-m-d');
$dateTo = $today->modify('-1 day')->format('Y-m-d');
$pwas = [];
for ($page = 1; ; $page++) {
$response = $api->get('pwas', ['query' => ['status' => 'active', 'page' => $page, 'excludes' => 'stats']]);
$data = json_decode($response->getBody(), true)['data'];
$pwas = [...$pwas, ...$data['pwas']];
if ($page >= $data['meta']['last_page']) {
break;
}
}
foreach ($pwas as $pwa) {
$response = $api->get('stats/funnel', ['query' => [
'date_from' => $dateFrom,
'date_to' => $dateTo,
'pwa_id' => $pwa['id'],
]]);
$totals = json_decode($response->getBody(), true)['data']['funnel']['totals'];
usleep(250000); // не больше 300 запросов в минуту
if ($totals['installs'] < $minInstalls || $totals['deps'] > 0) {
continue;
}
echo "{$pwa['id']} {$pwa['name']}: {$totals['installs']} установок, 0 депозитов" . PHP_EOL;
if (!$dryRun) {
$api->put("pwas/{$pwa['id']}", ['json' => ['active' => false, 'team_id' => $pwa['team']['id']]]);
echo ' выключено' . PHP_EOL;
}
}Скрипт делает по одному запросу статистики на каждое PWA и держит паузу 0,25 с, чтобы не упереться в лимит 300 запросов в минуту. Установки и депозиты здесь - по дате события, как в воронке (см. раздел «Статистика»). Команда участников PWA получит уведомление о каждом выключенном приложении.
Статистика в Google Таблицах
Каждое утро дописывает в Google Таблицу строку с итогами за вчера. За месяц получается готовая таблица для графиков и сводок - без серверов и cron, всё внутри Google.
Настройка:
- Откройте нужную Google Таблицу и выберите Extensions → Apps Script.
- Вставьте скрипт ниже вместо содержимого редактора и замените
panel.example.comна адрес своей панели. - Слева откройте Project Settings (шестерёнка) → Script Properties → Add script property: имя
PWA_HUB_KEY, значение - ваш ключ. Так ключ не хранится в тексте скрипта. - Сверху выберите функцию
appendYesterdayи нажмите Run. Google попросит разрешения - подтвердите. В таблице появится листPWA Hubс первой строкой. - Слева откройте Triggers (будильник) → Add Trigger: функция
appendYesterday, источник Time-driven, Day timer, время - например, 7am to 8am. Сохраните.
// Google Таблица → Extensions → Apps Script
const API = 'https://panel.example.com/api';
const SHEET = 'PWA Hub';
function appendYesterday() {
const key = PropertiesService.getScriptProperties().getProperty('PWA_HUB_KEY');
if (!key) throw new Error('Добавьте PWA_HUB_KEY в Script Properties');
const yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000);
const day = Utilities.formatDate(yesterday, 'UTC', 'yyyy-MM-dd');
const response = UrlFetchApp.fetch(`${API}/stats/dashboard?date_from=${day}&date_to=${day}`, {
headers: { Authorization: `Bearer ${key}`, Accept: 'application/json' },
muteHttpExceptions: true,
});
if (response.getResponseCode() !== 200) {
throw new Error(`PWA Hub ответил ${response.getResponseCode()}: ${response.getContentText()}`);
}
const metrics = JSON.parse(response.getContentText()).data.metrics;
const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
let sheet = spreadsheet.getSheetByName(SHEET);
if (!sheet) {
sheet = spreadsheet.insertSheet(SHEET);
sheet.appendRow(['Дата', 'Визиты', 'Уникальные', 'Установки', 'Регистрации', 'Депозиты', 'CR, %']);
}
sheet.appendRow([
day,
metrics.total_visits.value,
metrics.total_uniques.value,
metrics.total_installs.value,
metrics.total_registrations.value,
metrics.total_deposits.value,
metrics.avg_cr.value,
]);
}Повторный запуск - повторная строка
Скрипт просто дописывает строку. Запустили его дважды за день - вчерашний день окажется в таблице два раза; лишнюю строку удалите вручную.
Если панель недоступна или ключ отозван, запуск по триггеру завершится ошибкой, а Google пришлёт письмо о сбое - строка за этот день не появится. Пропущенный день можно дописать, поменяв yesterday на нужную дату и запустив функцию вручную.