La deuxième source de migration de notre déploiement Tier-3 a été lancée aujourd'hui. Collez une clé API Rebrandly, filtrez éventuellement par espace de travail, et cliquez sur Démarrer. Six à dix minutes plus tard, chaque slashtag se trouve sur votre domaine Elido, avec le slug préservé lorsqu'il n'y avait pas de collision. La migration depuis Bitly, arrivée il y a deux semaines, a posé les bases ; Rebrandly est le deuxième fournisseur à en bénéficier.
Cet article est le compte-rendu d'ingénierie — ce qui est spécifique à Rebrandly, ce que nous avons gardé identique au worker Bitly, et où l'API de Rebrandly a imposé une forme différente.
Ce qui est partagé avec Bitly#
Toute la fonctionnalité a toujours été prévue pour n'être qu'une table et un contrat de worker. Les deux ont tenu bon.
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,
-- compteurs + error_log + horodatages omis
);
source_vendor passe à rebrandly. source_filter contient {workspace_id: "..."} lorsque l'utilisateur filtre ; {} lorsqu'il souhaite tous les liens visibles avec la clé. Tout le reste — le budget de 30 minutes, le plafond de 50 000 liens, la stratégie de conflit suffix/skip/fail, le tag imported:rebrandly — est identique au chemin Bitly.
Le lanceur du tableau de bord (apps/web/src/app/dashboard/integrations/[id]/rebrandly-migration-launcher.tsx) est structurellement une copie de celui de Bitly, sans le menu déroulant des groupes — Rebrandly a des espaces de travail, pas des groupes, et nous les exposons comme un filtre textuel optionnel plutôt que comme un menu déroulant pré-rempli, car le point de terminaison Workspaces est paginé et non authentifié, et l'utilisateur typique en a au plus deux.
Là où l'API de Rebrandly diffère#
Trois choses :
Taille de page. Rebrandly limite une page à 25 liens. Bitly à 100. Ainsi, un compte de 5 000 liens qui se termine en 4 à 8 minutes sur Bitly prend 6 à 10 minutes sur Rebrandly. Le goulot d'étranglement est le fournisseur, pas le worker.
Pagination. Rebrandly utilise un paramètre de chaîne de requête last qui prend l'ID du dernier élément de la page précédente. Bitly renvoie une URL pagination.next. Les deux sont de type curseur ; celui de Rebrandly est juste un peu plus bavard. La boucle entière fait six lignes :
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
}
Nous faisons confiance au curseur. Si Rebrandly renvoie le même last deux fois, nous bouclerions indéfiniment ; le budget de 30 minutes limite les dégâts.
Portée de l'espace de travail. La clé API de Rebrandly voit tous les liens de tous les espaces de travail auxquels l'utilisateur appartient. Si vous avez un compte d'agence avec cinq espaces de travail clients, vous voulez presque certainement importer un par un. Le lanceur expose cela comme un champ textuel optionnel — collez l'ID de l'espace de travail depuis la barre d'URL de Rebrandly, ou laissez vide pour « tout ce que la clé voit ».
Ce que nous ne migrons pas#
Historique des clics. Les données par clic de Rebrandly sont réservées au niveau Premium et apparaissent sous forme de compteurs agrégés par lien, et non d'événements par clic. Nous mettons en avant cette limite sur chaque interface que l'utilisateur voit — la page de recette du tableau de bord, la page d'accueil /migrate-from/rebrandly, l'interface utilisateur de progression de l'importation, et la section FAQ. Les nouveaux clics atterrissent dans les analyses Elido à partir du moment de la bascule.
Modèles UTM Rebrandly. Ce sont des fonctionnalités de présentation dans Rebrandly qui n'ont pas de surface API propre pour l'exportation. Reconstruisez-les en tant que règles de campagne Elido — le tag imported:rebrandly est le point d'ancrage pour la réaffectation en masse.
Style QR. Le QR Elido par défaut est généré pour chaque lien importé ; les designs personnalisés doivent être réappliqués. La plupart des utilisateurs utilisent le filtre de tag en masse pour attribuer une superposition CTA Elido par défaut ou une campagne post-hoc.
Gestion des jetons#
Identique à Bitly. Le jeton n'atterrit jamais sur le disque :
bgCtx := context.WithoutCancel(r.Context())
go h.rebrandly.Run(bgCtx, job.ID, imports.RebrandlyJobOptions{
Token: req.Token,
WorkspaceID: req.WorkspaceID,
})
source_token_id reste NULL. La table service_tokens de l'ADR-0036 est destinée aux intégrations de jetons à coller Tier-2 (Mailchimp, Brevo, Klaviyo) où l'utilisation récurrente justifie la persistance. Pour les migrations ponctuelles, le stockage en mémoire uniquement est le bon compromis opérationnel — l'utilisateur colle le jeton une fois, le worker s'exécute, le jeton disparaît.
context.WithoutCancel (Go 1.21+) conserve les valeurs du contexte — logger, ID de trace, délai — mais supprime son signal d'annulation afin que le worker survive à la requête HTTP qui l'a déclenché. C'est le même pattern que le worker Bitly et le même pattern que chaque futur fournisseur de migration utilisera.
Résolution des conflits#
Trois stratégies, identiques à Bitly. L'utilisateur choisit au lancement du job :
- suffix (par défaut) : parcourt
mylink-2,mylink-3, … jusqu'à 50 candidats. Au-delà de 50, nous traitons cela comme un problème structurel et affichons une erreur. - skip : laisse le lien Elido existant intact, journalise la ligne source, compte comme ignoré.
- fail : annule tout le job dès le premier conflit. Pour une sémantique stricte de 1:1.
La recherche de slug est une lecture indexée par ligne :
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 suffix/skip/fail identique à bitly.go
}
Nous payons une lecture supplémentaire par ligne, mais obtenons un parcours de suffixe déterministe et un message d'erreur plus convivial. L'alternative — rechercher une violation d'unicité dans pgx et analyser le nom de la contrainte à partir de la chaîne d'erreur — est un moins bon compromis.
Ce qui est mesurable#
Mêmes journaux structurés zap que Bitly. Espace de travail, domaine cible, stratégie de conflit, filtre d'espace de travail optionnel. Les événements du cycle de vie du job — démarrage, achèvement, bascules de balayage de blocage — sont préexistants et le tableau de bord interroge le point de terminaison de scrutation toutes les deux secondes.
Nous ne traçons pas encore les métriques des jobs de migration en production. La cohorte Bitly nous a donné notre première référence de trafic réel ; les données de Rebrandly devraient être directement comparables car le worker est mécaniquement identique et les différences concernent la forme de pagination du fournisseur. Premier candidat à l'alerte : nombre de stuck-sweep > 0 dans n'importe quelle fenêtre d'une heure — cela signifie qu'un worker est mort et que l'interface utilisateur de l'utilisateur est bloquée sur running.
Reprenabilité et le problème du déploiement#
Même compromis que Bitly. Le worker est en cours de processus ; un déploiement en cours d'importation tue la goroutine. Nous acceptons cela pour la v1 car :
- La plupart des jobs se terminent en moins de dix minutes. Les déploiements sont peu fréquents aux heures d'importation.
- Le champ
import_jobs.last_progress_atplus un cron destuck-sweepde 5 minutes bascule toute lignerunningsans progression au cours des 30 dernières minutes enfailedavec une raison claire. - La relance est idempotente avec les stratégies de suffixe et d'ignorer — les liens déjà importés sont détectés lors du second passage et résolus selon la stratégie.
Pour les comptes au-delà de 10 000 liens, la reprenabilité prend tout son sens — nous enregistrons le curseur last de Rebrandly dans import_jobs.source_filter et reprenons là où la dernière exécution s'est arrêtée. C'est la prochaine itération ; les quatre autres sources de migration bénéficieront du même changement une fois que nous l'aurons lancé.
Quelle est la suite#
Même base, trois fournisseurs de plus à intégrer dans la même table import_jobs.
- Short.io —
GET /links?limit=150&domain_id=…. Pagination par domaine ; nous demandons à l'utilisateur de choisir un domaine source plutôt qu'un espace de travail. - Dub.co —
GET /api/links?projectSlug=…&limit=100. Dossiers + tags préservés ; c'est le plus propre des quatre. - TinyURL — API REST Pro/Bulk. TinyURL gratuit n'a pas d'API et n'en a jamais eu ; ce chemin reste manuel.
Chacun se retrouve derrière la même interface utilisateur de scrutation du tableau de bord et le même pattern de tag imported:<vendor>. Le worker spécifique au fournisseur reste dans services/api-core/internal/imports/<vendor>.go.
Si vous avez hésité à faire une comparaison avec Rebrandly parce que le chemin de migration n'était pas documenté, c'est désormais chose faite. Essayez-le — de la clé API au dernier lien importé en moins de dix minutes pour les comptes typiques.
Sur le même sujet sur le blog#
- Lancement de la migration depuis Bitly : un worker, un jeton, un budget de 30 minutes
- Migrer depuis Bitly sans casser les liens : un manuel des modes de défaillance
- Les liens courts comme Terraform : la pierre angulaire de l'ingénierie
- Pourquoi nous utilisons ClickHouse pour les analyses de clics (et pas Postgres)