Друге джерело міграції в нашому розгортанні Tier-3 було випущено сьогодні. Вставте API-ключ Rebrandly, за бажанням відфільтруйте за робочим простором і натисніть «Start». Через шість-десять хвилин кожен слештег опиняється на вашому домені Elido зі збереженим слагом, де не виникло конфліктів. Міграція з Bitly, яка з'явилася два тижні тому, заклала каркас; Rebrandly — другий вендор, що скористався ним.
Цей пост — інженерний звіт про те, що є специфічним для Rebrandly, що ми залишили ідентичним воркеру Bitly і де API Rebrandly змусило обрати іншу структуру.
Що спільного з Bitly#
Ця функція з самого початку планувалася як одна таблиця та один контракт воркера. Обидва витримали перевірку.
CREATE TABLE import_jobs (
id BIGSERIAL PRIMARY KEY,
workspace_id BIGINT NOT NULL,
source_vendor TEXT NOT NULL,
target_domain_id BIGINT NOT NULL,
status TEXT NOT NULL DEFAULT 'queued',
conflict_strategy TEXT NOT NULL DEFAULT 'suffix',
source_filter JSONB NOT NULL DEFAULT '{}'::jsonb,
-- counters + error_log + timestamps elided
);
source_vendor змінюється на rebrandly. source_filter містить {workspace_id: "..."}, коли користувач застосовує фільтр, або {}, коли потрібно імпортувати всі посилання, доступні за цим ключем. Все інше — 30-хвилинний бюджет, обмеження в 50 тис. посилань, стратегія вирішення конфліктів (суфікс/пропуск/помилка), тег imported:rebrandly — ідентичне до шляху Bitly.
Лаунчер на дашборді (apps/web/src/app/dashboard/integrations/[id]/rebrandly-migration-launcher.tsx) структурно є копією лаунчера для Bitly, але без випадаючого списку груп — у Rebrandly є робочі простори (workspaces), а не групи, і ми виставляємо їх як опціональний текстовий фільтр, а не як випадаючий список, оскільки ендпоінт Workspaces використовує пагінацію без автентифікації, а типовий користувач має щонайбільше два робочих простори.
У чому різниця API Rebrandly#
Три моменти:
Розмір сторінки. Rebrandly обмежує одну сторінку 25 посиланнями. Bitly — 100. Тому акаунт на 5000 посилань, який обробляється за 4–8 хвилин у Bitly, забере 6–10 хвилин у Rebrandly. Вузьким місцем є вендор, а не воркер.
Пагінація. Rebrandly використовує параметр запиту last, який приймає ID останнього елемента на попередній сторінці. Bitly повертає URL pagination.next. Обидва варіанти мають курсорний тип; варіант Rebrandly просто здійснює більше запитів. Весь цикл складається з шести рядків:
last := ""
for {
page, err := w.fetchPage(ctx, opts.Token, opts.WorkspaceID, last)
if err != nil { /* mark failed */ return }
if len(page) == 0 { break }
for _, link := range page {
// ... resolve slug, insert, update counters ...
}
last = page[len(page)-1].ID
}
Ми довіряємо курсору. Якби Rebrandly двічі повернув однаковий last, ми б зациклилися; 30-хвилинний бюджет обмежує шкоду.
Обмеження робочим простором. API-ключ Rebrandly бачить кожне посилання в кожному робочому просторі, до якого належить користувач. Якщо у вас агенційський акаунт із п'ятьма клієнтськими робочими просторами, ви майже напевно захочете імпортувати їх по одному. Лаунчер виставляє це як опціональне текстове поле — вставте ID робочого простору з адресного рядка Rebrandly або залиште порожнім, щоб імпортувати «все, що бачить ключ».
Що ми не мігруємо#
Історію кліків. Дані Rebrandly про кліки доступні лише для тарифів Premium і відображаються як сумарні лічильники на посилання, а не як події кліків. Ми акцентуємо на цьому обмеженні на кожній сторінці, яку бачить користувач — на сторінці рецепта на дашборді, на лендингу /migrate-from/rebrandly, в UI прогресу імпорту та в розділі FAQ. Нові кліки потрапляють в аналітику Elido з моменту перемикання.
UTM-шаблони Rebrandly. Це функція відображення в Rebrandly, яка не має чіткого API для експорту. Перестворіть їх як правила кампаній в Elido — тег imported:rebrandly є інструментом для масового перепризначення.
Стилізація QR. Стандартний QR-код Elido генерується для кожного імпортованого посилання; кастомний дизайн потрібно застосовувати повторно. Більшість користувачів використовують масовий фільтр за тегами, щоб застосувати накладання CTA або кампанії Elido постфактум.
Обробка токенів#
Ідентична до Bitly. Токен ніколи не записується на диск:
bgCtx := context.WithoutCancel(r.Context())
go h.rebrandly.Run(bgCtx, job.ID, imports.RebrandlyJobOptions{
Token: req.Token,
WorkspaceID: req.WorkspaceID,
})
source_token_id залишається NULL. Таблиця service_tokens з ADR-0036 призначена для інтеграцій Tier-2 із токенами, що вставляються (Mailchimp, Brevo, Klaviyo), де повторне використання виправдовує зберігання. Для одноразових міграцій використання лише в пам'яті є правильним операційним рішенням — користувач один раз вставляє токен, воркер виконує роботу, токен видаляється.
context.WithoutCancel (Go 1.21+) зберігає значення контексту — логер, trace ID, дедлайни — але видаляє сигнал скасування, щоб воркер пережив HTTP-запит, який його ініціював. Це той самий патерн, що й у воркера Bitly, і той самий патерн, який використовуватимуть усі майбутні вендори міграції.
Вирішення конфліктів#
Три стратегії, ідентичні до Bitly. Користувач обирає їх під час запуску завдання:
- suffix (за замовчуванням): додавати
mylink-2,mylink-3… до 50 варіантів. Після 50 ми вважаємо це структурною проблемою і видаємо помилку. - skip: залишити існуюче посилання Elido, залогувати джерело, зарахувати як пропущене.
- fail: перервати все завдання при першому конфлікті. Для суворої семантики 1:1.
Пошук слага — це одне індексоване читання на рядок:
func (w *RebrandlyWorker) resolveSlug(ctx context.Context, domainID int64, desired, strategy string) (string, error) {
if _, err := w.links.GetByDomainSlug(ctx, domainID, desired); err != nil {
if errors.Is(err, pgx.ErrNoRows) { return desired, nil }
return "", fmt.Errorf("slug lookup: %w", err)
}
// suffix/skip/fail branching identical to bitly.go
}
Ми платимо додатковим читанням на рядок, але отримуємо детермінований перебір суфіксів і більш зрозуміле повідомлення про помилку. Альтернатива — відловлювання порушення унікальності в pgx і розбір назви обмеження з рядка помилки — гірше рішення.
Що піддається вимірюванню#
Ті самі структуровані zap-логи, що й у Bitly. Робочий простір, цільовий домен, стратегія конфліктів, опціональний фільтр робочого простору. Події життєвого циклу завдання — старт, завершення, очищення завислих завдань — існують попередньо, і дашборд опитує ендпоінт кожні дві секунди.
Ми поки що не графікуємо метрики завдань міграції в продакшені. Когорта Bitly дала нам першу реальну базову лінію трафіку; дані Rebrandly мають бути прямо порівнянними, оскільки воркер механічно ідентичний, а відмінності полягають лише у формі пагінації вендора. Перший кандидат на алерт: кількість очищень завислих завдань > 0 за будь-яку годину — це означає, що воркер «впав», а UI користувача завис у стані running.
Відновлюваність і проблема деплою#
Та сама логіка, що й у Bitly. Воркер працює в процесі; деплой під час імпорту вбиває горутину. Ми приймаємо це для v1, тому що:
- Більшість завдань завершуються менш ніж за десять хвилин. Деплої рідкісні в час активного імпорту.
- Поле
import_jobs.last_progress_atплюс cron-завдання для очищення завислих завдань, що працює кожні 5 хвилин, переводить будь-який рядокrunningбез прогресу за останні 30 хвилин у станfailedіз чіткою причиною. - Повторний запуск є ідемпотентним при стратегіях suffix і skip — вже імпортовані посилання виявляються на другому проході та вирішуються згідно зі стратегією.
Для акаунтів, де посилань понад 10 000, відновлюваність виправдовує себе — ми записуємо курсор last від Rebrandly в import_jobs.source_filter і продовжуємо з того місця, де зупинилися. Це наступна ітерація; чотири інших джерела міграції отримають користь від тієї самої зміни, як тільки ми її випустимо.
Що далі#
Той самий каркас, ще три вендори, які будуть інтегровані в ту саму таблицю import_jobs.
- Short.io —
GET /links?limit=150&domain_id=…. Пагінація по доменах; ми попросимо користувача обрати джерельний домен, а не робочий простір. - Dub.co —
GET /api/links?projectSlug=…&limit=100. Збереження папок + тегів; це найчистіша реалізація з усіх чотирьох. - TinyURL — Pro/Bulk REST API. Безкоштовний TinyURL не має API і ніколи не мав; цей шлях залишається ручним.
Кожен із них буде працювати через той самий UI опитування на дашборді та патерн тегів imported:<vendor>. Специфічний для вендора воркер залишається в services/api-core/internal/imports/<vendor>.go.
Якщо ви відкладали порівняння з Rebrandly через те, що шлях міграції не був задокументований, тепер він задокументований. Спробуйте — від API-ключа до останнього імпортованого посилання менше ніж за десять хвилин для типових акаунтів.