П'яте і останнє джерело міграції в нашому розгортанні Tier-3 сьогодні запущено. Вставте Dub.co API токен, за бажанням відфільтруйте за слагом проєкту, натисніть Start. Через три-п'ять хвилин кожне посилання з'явиться на вашому домені Elido зі збереженим слагом, а структура папок Dub перетвориться на теги Elido.
Цей пост — інженерний звіт: що особливого в Dub, чому їхній API — найчистіший з п'яти вендорів, які ми підтримуємо, і що мотивує перехід з Dub на Elido.
Чому існує ця міграція#
Dub.co є найближчим open-source еквівалентом Elido за функціональністю. Сильний продукт, чистий REST API, сучасна панель управління. Цей шлях міграції призначений для команд, які обирають Elido з однієї з трьох причин:
- Проживання в ЄС. План даних Elido базується в ЄС — Hetzner FRA + ASH POPs, OVH SGP для APAC, всі події кліків за замовчуванням потрапляють у ClickHouse в регіоні ЄС. Dub Cloud базується в США; позиція GDPR/Schrems-II є компромісом.
- Розташування Edge POP. Три регіональні POP з p95 < 15мс кеш HIT — це інший цільовий показник затримки, ніж шлях Dub лише через Cloudflare-Workers. Робочі навантаження, чутливі до затримки (mobile-first, ad-attribution), відчувають різницю.
- Глибина аналітики. ClickHouse-базована аналітика кліків із утриманням на подію, перенаправленням конверсій у GA4/Meta CAPI та повним історичним відтворенням. Аналітика Dub чиста, але агрегована на PostgreSQL; межа глибини інша.
Якщо нічого з цього не стосується, Dub — чудовий продукт. Міграція призначена для команд, до яких це стосується.
Чому API Dub — найчистіший#
Ми вже створили воркери для п'яти API вендорів. Рейтинг за простотою інтеграції:
- Dub.co — bearer токен, помилки, сумісні з JSON-RFC, пагінація
?page=+?limit=100, кожне поле задокументовано прикладами корисного навантаження. - Short.io — чисто, явний логічний тип
HasMore, але партиціонування за доменами потребує роботи над UI/UX. - Bitly — URL
pagination.nextвідповідає стандартам; супровідна API документація є вичерпною. - TinyURL — тільки Pro/Bulk, решта не підтримується; документація мізерна.
- Rebrandly — курсор
?last=<id>нормальний, але ліміт 25 на сторінку робить усе повільним.
Перевага Dub: їхня документація включає приклади curl, відповіді на помилки містять і машинний код, і людське повідомлення, а їхня пагінація — той самий нудний тип, де ?page=2&limit=100 працює саме так, як ви очікуєте.
const dubPageSize = 100
page := 1
for {
resp, err := w.fetchPage(ctx, opts.Token, opts.WorkspaceID, page)
if err != nil { /* mark failed */ return }
if len(resp) == 0 { break }
for _, link := range resp { /* import */ }
if len(resp) < dubPageSize { break }
page++
}
Dub не повертає прапорець HasMore; ми робимо висновок про це за короткою сторінкою. Це стандартний шаблон пагінації REST, і він добре працює — сторінка, коротша за ліміт, означає, що ми закінчили.
Папки стають тегами#
У Dub є і папки, і теги як примітиви організації. У Elido є тільки теги. Тому міграція перетворює папки Dub на набір тегів:
tags := make([]string, 0, len(link.Tags)+2)
for _, t := range link.Tags {
tags = append(tags, t.Name)
}
if link.Folder != nil && link.Folder.Name != "" {
// Dub folders can nest; flatten the full path.
for _, segment := range strings.Split(link.Folder.Name, "/") {
seg := strings.TrimSpace(segment)
if seg != "" {
tags = append(tags, seg)
}
}
}
tags = append(tags, "imported:dub")
Посилання Dub у папці campaigns/q3-launch із тегами paid та linkedin імпортується з тегами paid, linkedin, campaigns, q3-launch та imported:dub. Семантика фільтрів в Elido обробляє ті самі патерни отримання, що й UI папок Dub — tag-equals, tag-contains, multi-tag-AND. Ми не винаходимо ієрархію папок на стороні сервера; користувач отримує плоский список тегів і примітиви фільтрів.
Чи могли ми замість цього додати папки в Elido? Так. Ми вибрали тільки теги, коли модель даних Elido була випущена у Фазі 1; папки мають сенс для ментальних моделей файлової системи робочого столу і менше сенсу для операцій із великою кількістю коротких посилань. Міграція користувачів Dub на теги Elido — це правильний бік цього компромісу.
Фільтрація проєктів#
Dub використовує "робочі простори" (у своєму новішому UI), а історично називав їх "проєктами". API приймає параметр workspaceId для фільтрації; лаунчер відображає його як необов'язкове текстове поле. Вставте слаг робочого простору з вашої URL-адреси Dub або залиште порожнім, щоб отримати всі посилання, які бачить токен.
Це відображає фільтр робочого простору Rebrandly та поле домену Short.io. Три з п'яти наших вендорів міграції мають концепцію партиціонування для кожного облікового запису; ми послідовно відображаємо це як необов'язкове текстове введення, а не як заповнений випадаючий список, оскільки типовий користувач має щонайбільше два робочі простори, а ендпоінт списку API додає затримку, яка не варта цієї поліровки.
Чого ми не мігруємо#
Правила Dub щодо геотаргетингу та таргетингу на пристрої. Це потужна функція Dub, але форма правил не відображається 1:1 на smart-link правила Elido. Слаг імпортується; перебудуйте правила, використовуючи синтаксис виразів Elido, який є більш дозвільним, але має іншу ментальну форму.
Історія по кліках. Універсальний ліміт для всіх п'яти джерел міграції. Дані Dub по кліках знаходяться за їхнім ендпоінтом аналітики, який обмежений планом тарифу; нові кліки потрапляють в аналітику Elido після переходу.
Стилізація QR. QR-код Elido за замовчуванням генерується заново; індивідуальний дизайн потрібно застосувати повторно. Тег imported:dub є дескриптором для масового перепризначення.
ACL робочого простору Dub та конфігурація ролей. Надайте доступ знову в Elido, використовуючи SCIM/SSO або запрошення членів робочого простору; модель ролей між двома продуктами відрізняється настільки, що автоматизоване відображення проявилося б як приховане підвищення привілеїв.
Self-hosted Dub#
Dub — це open-source, і самохостингові інстанси Dub є звичними. Міграція використовує той самий REST API, який відкриває продукт Dub Cloud, тому вказування на self-hosted Dub означає перезапис DUB_API_BASE. Ми не виставили це як self-serve налаштування у v1, оскільки операційний хвіст не є тривіальним — різні версії Dub відкривають трохи різні форми відповіді, і ми не хочемо випускати лаунчер, який мовчки видає 500-ту помилку на розгортанні Dub v0.7, коли цільовою протестованою версією є v0.9.
Для міграцій self-hosted Dub надішліть листа на [email protected] з вашою версією Dub, і ми проведемо одноразову консьєрж-міграцію. Як тільки ми побачимо достатньо версій у "дикій природі", перезапис стане self-serve налаштуванням панелі управління.
Обробка токенів#
Та сама семантика одноразового виконання, як і у чотирьох інших вендорів міграції:
bgCtx := context.WithoutCancel(r.Context())
go h.dub.Run(bgCtx, job.ID, imports.DubJobOptions{
Token: req.Token,
WorkspaceID: req.WorkspaceID,
})
source_token_id залишається NULL. Токен живе в пам'яті процесу api-core під час роботи воркера, видаляється після завершення. Без персистентності — це одноразова міграція, а не повторюваний запит до вендора.
context.WithoutCancel (Go 1.21+) підтримує життя воркера після запиту HTTP. Той самий патерн, що й у кожного іншого вендора міграції в цьому розгортанні.
Вирішення конфліктів та контракт воркера#
Ідентично до кожного іншого вендора. Перебір суфіксів mylink-2, mylink-3, …, до 50 кандидатів; пропуск залишає існуюче посилання Elido в спокої; помилка перериває роботу при першому конфлікті. Контракт воркера — MaxLinksPerImport=50_000, ImportRunBudget=30*time.Minute — є спільним для всіх п'яти.
Пошук — це одне індексоване читання на рядок за (domain_id, slug). Детермінований перебір суфіксів, зручніші помилки, ніж виловлювання порушень унікальності в pgx.
Що ми вимірювали щодо Bitly#
І Dub, і Bitly мігрують приблизно з тією ж пропускною здатністю — 100 посилань на сторінку, ~5 вставок/сек стабільно. Джерело з 5,000 посиланнями завершується за 4–7 хвилин для обох вендорів. Різниця, видима користувачеві — це досвід після імпорту: посилання, імпортовані з Dub, приходять зі структурованими «хлібними крихтами» папка-як-тег; посилання, імпортовані з Bitly, приходять тільки з тегом imported:bitly та будь-якими довільними тегами Bitly.
Відновлюваність та проблема розгортання#
Той самий компроміс, що й у перших чотирьох міграціях. Воркер працює в процесі; розгортання під час імпорту вбиває горутину. 5-хвилинний крон stuck-sweep переводить будь-який рядок running без прогресу за 30 хвилин у стан failed. Повторний запуск є ідемпотентним при стратегіях суфіксів та пропуску.
Для облікових записів з понад 10,000 посилань відновлюваність була б корисною — ми записували б курсор page Dub у import_jobs.source_filter і відновлювали б з останньої завершеної сторінки. Усі п'ять вендорів міграції поділяють той самий дизайн in-process; коли ми випустимо відновлюваність, усі п'ять отримають користь.
Що далі для розгортання Tier-3#
Tier-3 готово. П'ять вендорів міграції, одна спільна таблиця import_jobs, один спільний контракт воркера, один спільний UI опитування панелі управління, п'ять SEO сторінок, п'ять інженерних постів у блозі.
Що в черзі після Tier-3:
- Відновлюваність для облікових записів понад 10,000 посилань. Чекпоінтинг курсору для кожного вендора.
- Резервний варіант CSV-експорту для користувачів на планах із відкликаними токенами. Зараз доступно тільки через консьєрж-сервіс.
- Фундамент Tier-2 service_tokens — токени вендорів для багаторазового використання для Mailchimp, Brevo, Klaviyo. Шлях міграції підтвердив патерн JSONB
source_filter; Tier-2 потребує персистентних зашифрованих токенів, що є територією ADR-0036.
Якщо ви придивлялися до Elido з робочого простору Dub, історія міграції тепер задокументована. Спробуйте — від токена до останнього імпортованого посилання менш ніж за п'ять хвилин для типових облікових записів.
Пов'язане в блозі#
- Shipping the Bitly migration: a worker, a token, a 30-minute budget
- Shipping the Rebrandly migration: 25-per-page pagination
- Shipping the Short.io migration: per-domain pagination at 150/page
- Shipping the TinyURL migration: Pro/Bulk REST, no free-tier path
- Short links as Terraform: the engineering cornerstone