6 мин чтенияИнженерия

Релиз миграции с Rebrandly: постраничная пагинация по 25 элементов и 30-минутный бюджет

Как мы реализовали импорт из Rebrandly в Elido в один клик: почему выбрали такой размер страницы, UX фильтрации рабочих пространств и что мы сознательно не переносим.

Marius Voß
DevRel · edge infra
Схема пайплайна: REST API Rebrandly слева, данные проходят через импорт-воркер Elido в таблицу ссылок, с боковой панелью, перечисляющей численные гарантии воркера (лимит 50 тыс., 30-минутный бюджет, по 25 элементов на страницу, токен только в памяти)

Сегодня был выполнен второй релиз источника миграции в рамках нашего плана Tier-3. Вставьте API-ключ Rebrandly, при желании отфильтруйте рабочее пространство и нажмите «Начать». Через 6–10 минут каждый слэштег окажется в вашем домене Elido, а слаг сохранится там, где не было конфликтов. Миграция с Bitly, запущенная две недели назад, подготовила основу; Rebrandly стал вторым провайдером, который использует этот механизм.

Этот пост - технический разбор того, что специфично для Rebrandly, что мы оставили идентичным воркеру для Bitly и где API Rebrandly вынудил нас изменить подход.

Схема пайплайна: REST API Rebrandly отдает по 25 ссылок на страницу во внутрипроцессный воркер импорта Elido, который разрешает слаги и записывает строки в таблицу ссылок, пока курсор не вернет пустую страницу

Что общего с 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 или кампанию.

Двухколоночная таблица с тем, что переносится при импорте из Rebrandly (ссылки, сохраненные слаги, URL назначения, тег imported:rebrandly, стандартный QR, новые клики) и что остается позади (история кликов, шаблоны UTM, кастомное оформление QR)

Обработка токенов

Идентично 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.
Дерево решений от проверки существования slug до трех стратегий: suffix перебирает mylink-2 до mylink-50, skip оставляет существующую ссылку и логирует строку, fail прерывает всю задачу при первом конфликте

Поиск слага выполняется через одно индексированное чтение на строку:

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, потому что:

  1. Большинство задач завершаются менее чем за десять минут. Деплои случаются нечасто в моменты активного импорта.
  2. Поле import_jobs.last_progress_at плюс крон для очистки зависших задач каждые 5 минут переводит любую строку running без прогресса за последние 30 минут в статус failed с четкой причиной.
  3. Повторный запуск идемпотентен при стратегиях 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 в день

Попробуйте Elido

URL-сокращатель с хостингом в ЕС: собственные домены, глубокая аналитика, открытый API. Бесплатный тариф - без банковской карты.

Теги
rebrandly migration
url shortener
go worker
data migration
engineering
tier 3 integrations

Читать дальше