ad-analytics-hub.tabletki. Backfill готовий, cron активний (вт 08:00 Київ), Looker views в комплекті.
type: project
originSessionId: c120c8e1-db10-4b48-a8c7-53b2e93524a5Статус: 🟢 backfill виконаний (Бер 2 — Кві 26, 8 тижнів Mon-Sun), Python-loader працює, systemd timer активний з 30.04.2026.
Раз на тиждень (вівторок 08:00 Київ) забирає попередній повний тиждень Mon-Sun з двох звітів Producer API:
reportId=4ed497e3...) → ad-analytics-hub.tabletki.tabletki_salesreportId=72c243dc...) → ad-analytics-hub.tabletki.tabletki_utmIdempotent upsert: DELETE rows for (date_from, date_to) → INSERT нових. Repeat-safe.
Tabletki.ua Producer API (cab-producer.tabletki.ua)
│ Bearer JWT (clientId=270, login=APIadsanalyzer)
│ ▼ /api/ReportExecution/report?reportType=JSON&reportId={uuid}
│ ▼ body: {dateFrom, dateTo} ← НЕ передавати periodType (HTTP 500)
│
├─ Sales (≈390 SKU rows / week)
│ ▼ tabletki_loader.py --mode sales
│ ▼ flatten 20 fields per docs handoff
│ ▼ BQ: tabletki_sales (DATE+DATE+SkuId-based natural key)
│
└─ UTM (≈10-20 campaign rows / week, after ChildArray flatten)
▼ tabletki_loader.py --mode utm
▼ ChildArray (JSON string) → 1 row per (campaign,source,medium)
▼ BQ: tabletki_utm (DATE+DATE+utm_campaign+source+medium)
ad-analytics-hub.tabletki.tabletki_sales (20 полів):
date_from DATE REQUIRED, date_to DATE REQUIRED, period_type STRINGsku_id, product_code INT REQ, product_name STRING REQ, manufacturercategory_code INT, category_nameunits_sold INT, revenue_uah FLOAT, avg_price_uah FLOATcategory_revenue_uah, pct_category_revenue, pct_category_unitscategory_top1_units, category_top2_units, category_top3_unitsingested_at TIMESTAMP, source_file STRING (api для нових)ad-analytics-hub.tabletki.tabletki_utm (10 полів):
date_from DATE REQ, date_to DATE REQ, utm_campaign STRING REQutm_source STRING, utm_medium STRINGclicks INT, conversions INT, conversion_value FLOATingested_at TIMESTAMP, source_file STRINGLooker Studio views (4 шт., створені Desktop'ом раніше): v_delta_sales_daily, v_delta_sales_weekly, v_delta_sales_trend, v_market_share.
/srv/tools/tabletki/tabletki_loader.py — Python loader (≈250 рядків), реалізований 2026-04-30 на основі handoff від Desktop'а. Modes: sales | utm | both | auto-week. Аргументи: --from --to --dry-run. Bash-обгортки немає, кличеться напряму з systemd unit./srv/passepartout/tabletki/api.token — Bearer JWT (chmod 600, exp 2027-04-16, login APIadsanalyzer)/srv/passepartout/tabletki/api.token.meta — опис ключа (issued/exp/scopes/login/clientId)/srv/passepartout/google/ad-pipeline-worker-sa.json — SA для BQ writes (має bigquery.dataEditor + bigquery.jobUser на dataset tabletki, перевірено)Коли: щовівторка 08:00 за Києвом.
Чому вівторок: дані Tabletki за неділю стабілізуються з лагом ~24-48 год (внутрішня зміна замовлень/повернень). Понеділок ще «брудний», вівторок — уже чистий тиждень Mon-Sun.
Що пулиться щотижня: попередній повний Mon-Sun тиждень (8 днів тому → 2 дні тому). Один прохід = 2 API-запити (sales + UTM) + 4 BQ-запити (delete×2 + load×2). Тривалість: ~5-15 секунд залежно від відповіді API.
SLA на дані: доступні в BQ через ~30 секунд після успішного запуску.
Ручний refill: якщо вівторок-запуск пропущено (наприклад VPS down) — Persistent=true у timer-юніті означає systemd сам наздожене при наступному boot. Або вручну:
GOOGLE_APPLICATION_CREDENTIALS=/srv/passepartout/google/ad-pipeline-worker-sa.json \
/srv/tools/tabletki/tabletki_loader.py --mode auto-week
Backfill ширшого діапазону: --mode both --from YYYY-MM-DD --to YYYY-MM-DD — loader сам розіб'є на тижні Mon-Sun, чанками. Рекомендовано не більше 12 тижнів за раз (~5 хв роботи).
/etc/systemd/system/tabletki-weekly-sync.timer — OnCalendar=Tue *-*-* 08:00:00, Timezone=Europe/Kyiv, Persistent=true, RandomizedDelaySec=300 (рандомізація 5 хв щоб не бомбити API в одну точно секунду)/etc/systemd/system/tabletki-weekly-sync.service — Type=oneshot, ExecStart=/srv/tools/tabletki/tabletki_loader.py --mode auto-week, Environment="GOOGLE_APPLICATION_CREDENTIALS=/srv/passepartout/google/ad-pipeline-worker-sa.json"OnFailure=tabletki-fail-notify.service → надсилає alert у Telegram chat 312194208 (бот claude-bot, токен в /srv/passepartout/telegram/claude-bot.token). Текст алерту: ❌ tabletki-weekly-sync failed at HH:MM, see journalctl -u tabletki-weekly-sync.service.journalctl -u tabletki-weekly-sync.service, retention за замовчуванням systemd (4 тижні).v_delta_sales_daily/weekly/trend/market_share views. Авто-refresh.-- Весь обсяг продажів Delta Medical за останні 8 тижнів
SELECT date_from, ROUND(SUM(revenue_uah), 0) AS uah, SUM(units_sold) AS units
FROM `ad-analytics-hub.tabletki.tabletki_sales`
WHERE manufacturer LIKE '%Дельта%' AND period_type = 'week'
GROUP BY 1 ORDER BY 1;
-- Топ-10 SKU за останній доступний тиждень
SELECT product_name, units_sold, revenue_uah
FROM `ad-analytics-hub.tabletki.tabletki_sales`
WHERE date_to = (SELECT MAX(date_to) FROM `ad-analytics-hub.tabletki.tabletki_sales`)
AND manufacturer LIKE '%Дельта%'
ORDER BY revenue_uah DESC LIMIT 10;
-- UTM funnel за тиждень: clicks → conversions → revenue
SELECT utm_campaign, utm_source, utm_medium, clicks, conversions, conversion_value
FROM `ad-analytics-hub.tabletki.tabletki_utm`
WHERE date_from = '2026-04-20'
ORDER BY clicks DESC;
| Симптом | Причина | Recovery |
|---|---|---|
OnFailure алерт у Telegram |
будь-який не-нуль exit | journalctl -u tabletki-weekly-sync.service -e -n 100 → побачити причину |
| 401 Unauthorized | токен прострочений (раз на рік) | оновити токен на cab-producer.tabletki.ua → перезаписати /srv/passepartout/tabletki/api.token (chmod 600) → не треба перезавантажувати — loader читає при кожному запуску |
| 400 validation, min_date | API не пускає UTM раніше 2026-03-01 | norm — loader просто пропускає такі тижні і йде далі |
| 500 Internal Server Error | передано periodType body field |
в loader НЕ передається, але якщо хтось доробить — пам'ятати |
| 0 рядків API повертає | діапазон у майбутньому або помилка dateFrom > dateTo |
перевірити cron OnCalendar |
BQ Access Denied |
SA втратив роль | gcloud projects add-iam-policy-binding ad-analytics-hub --member=serviceAccount:ad-pipeline-worker@... --role=roles/bigquery.dataEditor (потрібен власник проекту, бо ад-pipeline-worker SA living в іншому проекті) |
| Дублі рядків у BQ | пропущено DELETE перед INSERT | loader робить це автоматично, але якщо хтось через bq load --append — почистити вручну SQL DELETE |
periodType body field — викликає HTTP 500. НЕ передавати, тип періоду визначається з ширини діапазону.min_date validation)dateFrom-dateTo ≤ 31 день. Для більших — chunk weekly./srv/passepartout/tabletki/api.token.# Один тиждень (для cron — auto-detect Mon-Sun попереднього тижня)
GOOGLE_APPLICATION_CREDENTIALS=/srv/passepartout/google/ad-pipeline-worker-sa.json \
/srv/tools/tabletki/tabletki_loader.py --mode auto-week
# Backfill range (chunk by Mon-Sun automatically)
... --mode both --from 2026-03-01 --to 2026-04-29
# Тільки sales або тільки utm
... --mode sales --from 2026-04-13 --to 2026-04-19
... --mode utm --from 2026-04-20 --to 2026-04-26
# Dry-run (без запису в BQ)
... --mode utm --from 2026-04-20 --to 2026-04-26 --dry-run
tabletki_sales: 3,135 рядків, 8 тижнів Mon-Sun: 2026-03-02 → 2026-04-26tabletki_utm: 134 рядки, 8 тижнів Mon-Sun: 2026-03-02 → 2026-04-26GOOGLE_APPLICATION_CREDENTIALS=/srv/passepartout/google/ad-pipeline-worker-sa.json python3 -c "
from google.cloud import bigquery
client = bigquery.Client(project='ad-analytics-hub')
for tbl in ('tabletki_sales', 'tabletki_utm'):
q = f'SELECT date_from, date_to, COUNT(*) cnt FROM \\\`ad-analytics-hub.tabletki.{tbl}\\\` GROUP BY 1,2 ORDER BY 1'
print(tbl)
for r in client.query(q).result():
print(f' {r.date_from} → {r.date_to}: {r.cnt}')
"
desktop-memory/snapshot/project_tabletki_sales_integration.md)processed/read-2026-04-30T...-tabletki-pipeline-v{1,2}.md)ad-analytics-hub, region EU