Elido
7 min de leituraEngenharia

Lançando a migração do Rebrandly: paginação de 25 por página e um orçamento de 30 minutos

Como construímos importações do Rebrandly com um clique para o Elido — o tamanho lento da página, a UX do filtro de workspace e o que deliberadamente não migramos.

Marius Voß
DevRel · edge infra
Diagrama do pipeline: API REST do Rebrandly à esquerda fluindo através do worker de importação do Elido para a tabela de links, com um painel lateral listando as garantias numéricas que o worker possui (limite de 50 mil, orçamento de 30 min, 25 por página, token apenas na memória)

A segunda fonte de migração em nossa implementação de Tier-3 foi lançada hoje. Cole uma chave de API do Rebrandly, filtre opcionalmente por um workspace, e clique em Start. Seis a dez minutos depois, cada slashtag está no seu domínio do Elido com o slug preservado onde não houve colisão. A migração do Bitly que chegou há duas semanas definiu a estrutura; o Rebrandly é o segundo fornecedor a utilizá-la.

Este post é a documentação de engenharia — o que é específico do Rebrandly, o que mantivemos idêntico ao worker do Bitly e onde a API do Rebrandly forçou um formato diferente.

O que é compartilhado com o Bitly#

O recurso completo sempre seria uma tabela e um contrato de worker. Ambos se mantiveram.

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,
    -- contadores + error_log + timestamps omitidos
);

source_vendor muda para rebrandly. source_filter carrega {workspace_id: "..."} quando o usuário filtra; {} quando eles querem todos os links que a chave pode ver. Todo o resto — o orçamento de 30 minutos, o limite de 50 mil links, a estratégia de conflito suffix/skip/fail, a tag imported:rebrandly — é idêntico ao caminho do Bitly.

O lançador do dashboard (apps/web/src/app/dashboard/integrations/[id]/rebrandly-migration-launcher.tsx) é estruturalmente uma cópia do Bitly com o menu suspenso de grupos removido — o Rebrandly tem workspaces, não grupos, e os expomos como um filtro de texto opcional em vez de um menu suspenso populado, porque o endpoint de Workspaces é paginado sem autenticação e o usuário típico tem no máximo dois.

Onde a API do Rebrandly difere#

Três coisas:

Tamanho da página. O Rebrandly limita uma única página a 25 links. O Bitly faz 100. Então, uma conta de 5.000 links que termina em 4–8 minutos no Bitly leva 6–10 no Rebrandly. O gargalo é o fornecedor, não o worker.

Paginação. O Rebrandly usa um parâmetro de query-string last que recebe o ID do último item na página anterior. O Bitly retorna uma URL pagination.next. Ambos são estilo cursor; o do Rebrandly é apenas um pouco mais verboso. O loop inteiro tem seis linhas:

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
}

Confiamos no cursor. Se o Rebrandly retornar o mesmo last duas vezes, entraríamos em loop infinito; o orçamento de 30 minutos limita o dano.

Escopo de workspace. A chave de API do Rebrandly vê todos os links em todos os workspaces aos quais o usuário pertence. Se você tem uma conta de agência com cinco workspaces de clientes, você quase certamente deseja importar um por vez. O lançador expõe isso como um campo de texto opcional — cole o ID do workspace da barra de URL do Rebrandly, ou deixe em branco para "tudo o que a chave vê".

O que não migramos#

Histórico de cliques. Os dados por clique do Rebrandly são exclusivos do nível Premium e aparecem como contadores agregados por link, não eventos por clique. Expomos esse limite em todas as interfaces que o usuário vê — a página de receita do dashboard, a landing /migrate-from/rebrandly, a UI de progresso da importação e a seção de FAQ. Novos cliques caem no analytics do Elido a partir do momento da transição.

Templates UTM do Rebrandly. Eles são um recurso de tempo de apresentação no Rebrandly que não possui uma superfície de API limpa para exportação. Reconstrua-os como regras de campanha do Elido — a tag imported:rebrandly é o manipulador de reatribuição em massa.

Estilo de QR. O QR padrão do Elido é gerado para cada link importado; designs personalizados precisam ser reaplicados. A maioria dos usuários usa o filtro de tag em massa para atribuir um overlay de CTA padrão do Elido ou campanha post-hoc.

Tratamento de token#

Idêntico ao Bitly. O token nunca é gravado no disco:

bgCtx := context.WithoutCancel(r.Context())
go h.rebrandly.Run(bgCtx, job.ID, imports.RebrandlyJobOptions{
    Token:       req.Token,
    WorkspaceID: req.WorkspaceID,
})

source_token_id permanece NULL. A tabela service_tokens do ADR-0036 é para as integrações de paste-token de Tier-2 (Mailchimp, Brevo, Klaviyo) onde o uso recorrente justifica a persistência. Para migrações de uso único, apenas em memória é a troca operacional correta — o usuário cola o token uma vez, o worker roda, o token desaparece.

context.WithoutCancel (Go 1.21+) mantém os valores do contexto — logger, IDs de rastreamento, deadline — mas remove seu sinal de cancelamento para que o worker sobreviva à requisição HTTP que o iniciou. Este é o mesmo padrão do worker do Bitly e o mesmo padrão que todo futuro fornecedor de migração usará.

Resolução de conflitos#

Três estratégias, idênticas ao Bitly. O usuário escolhe quando inicia o job:

  • suffix (padrão): percorre mylink-2, mylink-3, ... até 50 candidatos. Acima de 50, tratamos como um problema estrutural e exibimos um erro.
  • skip: deixa o link existente do Elido como está, registra a linha de origem, conta como pulado.
  • fail: aborta o job inteiro no primeiro conflito. Para semântica estrita de 1:1.

A busca de slug é uma leitura indexada por linha:

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)
    }
    // branching de suffix/skip/fail idêntico ao bitly.go
}

Pagamos uma leitura extra por linha, mas obtemos uma caminhada de sufixo determinística e uma mensagem de erro mais amigável. A alternativa — procurar por uma violação de unicidade no pgx e analisar o nome da restrição na string de erro — é a pior troca.

O que é mensurável#

Mesmos logs estruturados de zap que o Bitly. Workspace, domínio de destino, estratégia de conflito, filtro opcional de workspace. Eventos do ciclo de vida do job — start, complete, stuck-sweep flips — são pré-existentes e o dashboard acessa o endpoint de polling a cada dois segundos.

Ainda não estamos plotando as métricas de job de migração em produção. A coorte do Bitly nos deu nossa primeira linha de base de tráfego real; os dados do Rebrandly devem ser diretamente comparáveis porque o worker é mecanicamente idêntico e as diferenças estão no formato de paginação do fornecedor. Primeiro candidato a alerta: contagem de stuck-sweep > 0 em qualquer janela de uma hora — isso significa que um worker morreu e a UI do usuário está travada em running.

Resumibilidade e o problema de deploy#

Mesma troca que o Bitly. O worker é em-processo; um deploy no meio da importação mata a goroutine. Aceitamos isso para a v1 porque:

  1. A maioria dos jobs termina em menos de dez minutos. Deploys são infrequentes nos horários de importação do dia.
  2. O campo import_jobs.last_progress_at mais um cron de stuck-sweep de 5 minutos altera qualquer linha running sem progresso nos últimos 30 minutos para failed com um motivo claro.
  3. Re-executar é idempotente sob as estratégias de suffix e skip — links já importados são detectados na segunda passagem e resolvidos de acordo com a estratégia.

Para contas acima de 10.000 links, a resumibilidade se paga — registramos o cursor last do Rebrandly no import_jobs.source_filter e continuamos de onde a última execução parou. Essa é a próxima iteração; as quatro outras fontes de migração se beneficiarão da mesma mudança assim que a lançarmos.

O que vem a seguir#

Mesma estrutura, mais três fornecedores para entrar na mesma tabela import_jobs.

  • Short.ioGET /links?limit=150&domain_id=…. Paginação por domínio; pedimos ao usuário para escolher um domínio de origem em vez de um workspace.
  • Dub.coGET /api/links?projectSlug=…&limit=100. Pastas + tags preservadas; este é o mais limpo dos quatro.
  • TinyURL — API REST Pro/Bulk. O TinyURL gratuito não tem API e nunca teve; esse caminho permanece manual.

Cada um entra atrás da mesma UI de polling do dashboard e do mesmo padrão de tag imported:<vendor>. O worker específico do fornecedor permanece em services/api-core/internal/imports/<vendor>.go.

Se você estava adiando uma comparação com o Rebrandly porque o caminho de migração não estava documentado, agora está. Experimente — da chave de API ao último link importado em menos de dez minutos para contas típicas.

Relacionado no blog#

Experimente o Elido

Encurtador de URL hospedado na UE: domínios personalizados, análises profundas e API aberta. Plano gratuito — sem cartão de crédito.

Tags
rebrandly migration
url shortener
go worker
data migration
engineering
tier 3 integrations

Continuar lendo