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:
- A maioria dos jobs termina em menos de dez minutos. Deploys são infrequentes nos horários de importação do dia.
- O campo
import_jobs.last_progress_atmais um cron de stuck-sweep de 5 minutos altera qualquer linharunningsem progresso nos últimos 30 minutos parafailedcom um motivo claro. - 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.io —
GET /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.co —
GET /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.