Сегодня был выполнен второй релиз источника миграции в рамках нашего плана Tier-3. Вставьте API-ключ Rebrandly, при желании отфильтруйте рабочее пространство и нажмите «Начать». Через 6–10 минут каждый слэштег окажется в вашем домене 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,
-- счетчики + error_log + метки времени опущены
);
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 ссылок, который мигрирует с Bitly за 4–8 минут, на Rebrandly займет 6–10 минут. Узкое место здесь - провайдер, а не воркер.
Пагинация. Rebrandly использует параметр запроса last, который принимает ID последнего элемента на предыдущей странице. Bitly возвращает URL pagination.next. Оба способа основаны на курсорах; у Rebrandly он просто чуть более «разговорчивый». Весь цикл состоит из шести строк:
last := ""
for {
page, err := w.fetchPage(ctx, opts.Token, opts.WorkspaceID, last)
if err != nil { /* пометить как failed */ return }
if len(page) == 0 { break }
for _, link := range page {
// ... разрешить слаг, вставить, обновить счетчики ...
}
last = page[len(page)-1].ID
}
Мы доверяем курсору. Если Rebrandly дважды вернет один и тот же last, мы попадем в бесконечный цикл; 30-минутный бюджет ограничивает возможный ущерб.
Область видимости рабочего пространства. API-ключ Rebrandly видит каждую ссылку в каждом рабочем пространстве, к которому принадлежит пользователь. Если у вас агентский аккаунт с пятью клиентскими рабочими пространствами, вы почти наверняка захотите импортировать их по одному. Лаунчер предоставляет это как опциональное текстовое поле - вставьте ID рабочего пространства из адресной строки Rebrandly или оставьте пустым для импорта «всего, что видит ключ».
Что мы не переносим
Историю кликов. Данные о кликах в Rebrandly доступны только в премиум-тарифах и представлены в виде агрегированных счетчиков для каждой ссылки, а не в виде событий по каждому клику. Мы сообщаем об этом ограничении везде, где пользователь может это увидеть: на странице рецепта в панели управления, на лендинге /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 предназначена для интеграций Tier-2 с постоянными токенами (Mailchimp, Brevo, Klaviyo), где их повторное использование оправдывает хранение. Для миграций, выполняемых один раз, хранение только в памяти - правильный операционный компромисс: пользователь вставляет токен один раз, воркер выполняет работу, токен исчезает.
context.WithoutCancel (Go 1.21+) сохраняет значения контекста - логгер, трассировочные 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 идентично bitly.go
}
Мы платим дополнительным чтением на строку, но получаем детерминированный перебор суффиксов и более понятное сообщение об ошибке. Альтернатива - отлавливать нарушение уникальности в pgx и парсить имя ограничения из строки ошибки - выглядит как худший компромисс.
Что можно измерить
Те же структурированные логи zap, что и у Bitly. Рабочее пространство, целевой домен, стратегия конфликтов, опциональный фильтр рабочего пространства. События жизненного цикла задачи - запуск, завершение, очистка зависших - уже существовали, и панель управления опрашивает эндпоинт статуса каждые две секунды.
Мы пока не строим графики метрик задач миграции в продакшене. Когорта Bitly дала нам первый реальный базовый уровень трафика; данные Rebrandly должны быть напрямую сравнимы, так как воркер механически идентичен, а отличия заключаются только в форме пагинации провайдера. Первый кандидат на оповещение: количество зависших задач > 0 в любом часовом окне - это означает, что воркер упал, а UI пользователя застрял на статусе running.
Возобновляемость и проблема деплоя
Тот же компромисс, что и у Bitly. Воркер работает в том же процессе; деплой во время импорта убивает горутину. Мы принимаем это для v1, потому что:
- Большинство задач завершаются менее чем за десять минут. Деплои случаются нечасто в моменты активного импорта.
- Поле
import_jobs.last_progress_atплюс крон для очистки зависших задач каждые 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-ключа до последней импортированной ссылки занимает менее десяти минут.
Похожее в блоге
Попробуйте Elido
Вставьте URL - получите короткую ссылку
Без регистрации. Ссылка живёт 30 дней. Зарегистрируйтесь, чтобы оставить её навсегда.
Бесплатно, без регистрации · 2 в день