La seconda fonte di migrazione per il nostro rollout di Tier-3 è stata rilasciata oggi. Incolla una chiave API di Rebrandly, filtra facoltativamente per un workspace e fai clic su Start. Da sei a dieci minuti dopo, ogni slashtag si trova sul tuo dominio Elido con lo slug preservato dove non c'erano collisioni. La migrazione da Bitly, rilasciata due settimane fa, ha gettato le basi; Rebrandly è il secondo fornitore a utilizzarle.
Questo post è il resoconto tecnico: cosa è specifico per Rebrandly, cosa abbiamo mantenuto identico al worker di Bitly e dove l'API di Rebrandly ha imposto una forma diversa.
Cosa è condiviso con Bitly#
L'intera funzionalità doveva essere composta da una sola tabella e un solo contratto di worker. Entrambi hanno retto.
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,
-- counters + error_log + timestamps elided
);
source_vendor diventa rebrandly. source_filter trasporta {workspace_id: "..."} quando l'utente filtra; {} quando desidera ogni link visibile dalla chiave. Tutto il resto — il budget di 30 minuti, il limite di 50.000 link, la strategia di conflitto per suffisso/salto/fallimento, il tag imported:rebrandly — è identico al percorso di Bitly.
Il launcher della dashboard (apps/web/src/app/dashboard/integrations/[id]/rebrandly-migration-launcher.tsx) è strutturalmente una copia di quello di Bitly con il menu a discesa dei gruppi rimosso: Rebrandly ha workspace, non gruppi, e li esponiamo come filtro di testo facoltativo anziché come menu a discesa popolato, perché l'endpoint Workspaces è impaginato e non autenticato e l'utente tipico ne ha al massimo due.
Dove differisce l'API di Rebrandly#
Tre cose:
Dimensione della pagina. Rebrandly limita una singola pagina a 25 link. Bitly ne gestisce 100. Quindi, un account da 5.000 link che termina in 4–8 minuti su Bitly ne richiede 6–10 su Rebrandly. Il collo di bottiglia è il fornitore, non il worker.
Impaginazione. Rebrandly utilizza un parametro di query-string last che accetta l'ID dell'ultimo elemento della pagina precedente. Bitly restituisce un URL pagination.next. Entrambi sono in stile cursore; quello di Rebrandly è solo leggermente più prolisso. L'intero ciclo è di sei righe:
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
}
Ci fidiamo del cursore. Se Rebrandly restituisse lo stesso last due volte, looparemmo all'infinito; il budget di 30 minuti limita i danni.
Scoping del workspace. La chiave API di Rebrandly vede ogni link in ogni workspace a cui appartiene l'utente. Se hai un account agenzia con cinque workspace cliente, quasi certamente vorrai importarne uno alla volta. Il launcher espone questo come un campo di testo facoltativo: incolla l'ID del workspace dalla barra degli URL di Rebrandly, oppure lascia vuoto per "tutto ciò che la chiave vede".
Cosa non migriamo#
Cronologia dei clic. I dati per clic di Rebrandly sono riservati al livello Premium e si presentano come contatori aggregati per link, non come eventi per singolo clic. Mostriamo questo limite in ogni punto in cui l'utente guarda: la pagina della ricetta nella dashboard, la landing page /migrate-from/rebrandly, l'interfaccia di avanzamento dell'importazione e la sezione FAQ. I nuovi clic finiscono nelle analisi di Elido dal momento del passaggio in poi.
Template UTM di Rebrandly. Sono una funzionalità di presentazione in Rebrandly che non ha una superficie API pulita per l'esportazione. Ricostruiscili come regole di campagna di Elido: il tag imported:rebrandly è l'handle per la riassegnazione in blocco.
Stile QR. Il QR predefinito di Elido viene generato per ogni link importato; i design personalizzati devono essere riapplicati. La maggior parte degli utenti utilizza il filtro per tag in blocco per assegnare un overlay CTA o una campagna predefinita di Elido a posteriori.
Gestione dei token#
Identica a Bitly. Il token non viene mai scritto su disco:
bgCtx := context.WithoutCancel(r.Context())
go h.rebrandly.Run(bgCtx, job.ID, imports.RebrandlyJobOptions{
Token: req.Token,
WorkspaceID: req.WorkspaceID,
})
source_token_id rimane NULL. La tabella service_tokens di ADR-0036 è per le integrazioni di Tier-2 con token incollati (Mailchimp, Brevo, Klaviyo) in cui l'uso ricorrente giustifica la persistenza. Per le migrazioni una tantum, l'uso esclusivo in memoria è il compromesso operativo corretto: l'utente incolla il token una volta, il worker viene eseguito, il token scompare.
context.WithoutCancel (Go 1.21+) mantiene i valori del contesto — logger, ID di traccia, scadenza — ma rimuove il suo segnale di cancellazione in modo che il worker sopravviva alla richiesta HTTP che lo ha avviato. Questo è lo stesso pattern del worker di Bitly e lo stesso pattern che utilizzerà ogni futuro fornitore di migrazione.
Risoluzione dei conflitti#
Tre strategie, identiche a Bitly. L'utente sceglie quando avvia il job:
- suffix (predefinito): percorre
mylink-2,mylink-3, … fino a 50 candidati. Oltre i 50, lo trattiamo come un problema strutturale e mostriamo un errore. - skip: lascia intatto il link Elido esistente, registra la riga di origine, conta come ignorato.
- fail: interrompe l'intero job al primo conflitto. Per una semantica rigorosa 1:1.
La ricerca dello slug è una lettura indicizzata per riga:
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 branching identical to bitly.go
}
Paghiamo una lettura extra per riga ma otteniamo un percorso di suffisso deterministico e un messaggio di errore più amichevole. L'alternativa — cercare una violazione di unicità in pgx e analizzare il nome del vincolo dalla stringa di errore — è un compromesso peggiore.
Cosa è misurabile#
Stessi log strutturati di Zap come Bitly. Workspace, dominio di destinazione, strategia di conflitto, filtro workspace facoltativo. Gli eventi del ciclo di vita del job — avvio, completamento, pulizia dei blocchi — sono preesistenti e la dashboard interroga l'endpoint di polling ogni due secondi.
Non stiamo ancora rappresentando graficamente le metriche del job di migrazione in produzione. La coorte di Bitly ci ha fornito la nostra prima baseline di traffico reale; i dati di Rebrandly dovrebbero essere direttamente confrontabili perché il worker è meccanicamente identico e le differenze riguardano la forma dell'impaginazione del fornitore. Primo candidato per l'avviso: numero di blocchi rimasti > 0 in qualsiasi finestra di un'ora: ciò significa che un worker è morto e l'interfaccia utente dell'utente è bloccata su running.
Riprendibilità e il problema del deploy#
Stesso compromesso di Bitly. Il worker è in-process; un deploy a metà importazione uccide la goroutine. Lo accettiamo per la v1 perché:
- La maggior parte dei job termina in meno di dieci minuti. I deploy sono rari negli orari di importazione.
- Il campo
import_jobs.last_progress_atpiù un cron di pulizia dei blocchi di 5 minuti trasforma qualsiasi rigarunningsenza progressi negli ultimi 30 minuti infailedcon una causa chiara. - La riesecuzione è idempotente con le strategie suffix e skip: i link già importati vengono rilevati al secondo passaggio e risolti secondo la strategia.
Per account con più di 10.000 link, la riprendibilità si ripaga da sola: registriamo il cursore last di Rebrandly in import_jobs.source_filter e riprendiamo da dove si era interrotta l'ultima esecuzione. Questa è la prossima iterazione; le altre quattro fonti di migrazione beneficeranno della stessa modifica una volta che la rilasceremo.
Cosa ci aspetta#
Stessa impalcatura, altri tre fornitori da inserire nella stessa tabella import_jobs.
- Short.io —
GET /links?limit=150&domain_id=…. Impaginazione per dominio; chiediamo all'utente di scegliere un dominio di origine piuttosto che un workspace. - Dub.co —
GET /api/links?projectSlug=…&limit=100. Cartelle + tag preservati; questo è il più pulito dei quattro. - TinyURL — API REST Pro/Bulk. La versione gratuita di TinyURL non ha API e non l'ha mai avuta; quel percorso rimane manuale.
Ognuno approda dietro la stessa interfaccia utente di polling della dashboard e lo stesso pattern di tag imported:<vendor>. Il worker specifico del fornitore rimane in services/api-core/internal/imports/<vendor>.go.
Se hai rimandato un confronto con Rebrandly perché il percorso di migrazione non era documentato, ora lo è. Provaci: dalla chiave API al link importato in meno di dieci minuti per gli account tipici.