Elido
7 min di letturaIngegneria

Rilasciamo la migrazione da Rebrandly: impaginazione a 25 elementi per pagina e budget di 30 minuti

Come abbiamo creato le importazioni da Rebrandly con un clic per Elido: la dimensione lenta delle pagine, l'UX del filtro dei workspace e cosa abbiamo scelto deliberatamente di non migrare.

Marius Voß
DevRel · edge infra
Diagramma della pipeline: API REST di Rebrandly a sinistra che fluisce attraverso il worker di importazione di Elido nella tabella dei link, con un pannello laterale che elenca le garanzie numeriche del worker (limite di 50k, budget di 30 min, 25 per pagina, token solo in memoria)

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é:

  1. La maggior parte dei job termina in meno di dieci minuti. I deploy sono rari negli orari di importazione.
  2. Il campo import_jobs.last_progress_at più un cron di pulizia dei blocchi di 5 minuti trasforma qualsiasi riga running senza progressi negli ultimi 30 minuti in failed con una causa chiara.
  3. 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.ioGET /links?limit=150&domain_id=…. Impaginazione per dominio; chiediamo all'utente di scegliere un dominio di origine piuttosto che un workspace.
  • Dub.coGET /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.

Correlati sul blog#

Prova Elido

Accorciatore di URL ospitato nell'UE: domini personalizzati, analisi approfondite e API aperta. Piano gratuito — senza carta di credito.

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

Continua a leggere