Elido
6 Min. LesezeitEngineering

Die Rebrandly-Migration bereitstellen: 25-pro-Seite-Pagining und ein 30-Minuten-Budget

Wie wir One-Click-Rebrandly-Importe für Elido gebaut haben – die langsame Seitengröße, die Workspace-Filter-UX und was wir bewusst nicht migrieren.

Marius Voß
DevRel · edge infra
Pipeline-Diagramm: Rebrandly REST API links, die durch den Elido-Import-Worker in die Links-Tabelle fließt, mit einem Seitenpanel, das die numerischen Garantien des Workers auflistet (50k-Limit, 30-Minuten-Budget, 25 pro Seite, Token nur im Speicher)

Die zweite Migrationsquelle in unserem Tier-3-Rollout wurde heute bereitgestellt. Fügen Sie einen Rebrandly API-Key ein, filtern Sie optional nach einem Workspace und klicken Sie auf Start. Sechs bis zehn Minuten später sitzt jeder Slashtag auf Ihrer Elido-Domain, wobei der Slug beibehalten wurde, sofern er nicht kollidierte. Die Bitly-Migration, die vor zwei Wochen live ging, legte das Gerüst; Rebrandly ist der zweite Anbieter, der darauf aufbaut.

Dieser Beitrag ist der technische Bericht – was spezifisch für Rebrandly ist, was wir identisch zum Bitly-Worker gehalten haben und wo die Rebrandly-API eine andere Form erzwang.

Was wir mit Bitly teilen#

Das gesamte Feature sollte schon immer auf einer Tabelle und einem Worker-Vertrag basieren. Beides hat sich bewährt.

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 wechselt zu rebrandly. source_filter trägt {workspace_id: "..."}, wenn der Benutzer filtert; {} wenn er jeden Link sehen will, den der Key sehen kann. Alles andere – das 30-Minuten-Budget, das 50k-Link-Limit, die Suffix/Skip/Fail-Konfliktstrategie, der imported:rebrandly-Tag – ist identisch mit dem Bitly-Pfad.

Der Dashboard-Launcher (apps/web/src/app/dashboard/integrations/[id]/rebrandly-migration-launcher.tsx) ist strukturell eine Kopie des Bitly-Launchers, wobei das Gruppen-Dropdown entfernt wurde – Rebrandly hat Workspaces, keine Gruppen, und wir stellen sie als optionalen Textfilter statt als ausgefülltes Dropdown bereit, da der Workspaces-Endpunkt nicht authentifiziert paginiert ist und der typische Benutzer höchstens zwei hat.

Wo sich die Rebrandly-API unterscheidet#

Drei Dinge:

Seitengröße. Rebrandly begrenzt eine einzelne Seite auf 25 Links. Bitly auf 100. Daher benötigt ein 5.000-Link-Account, der bei Bitly in 4–8 Minuten fertig ist, bei Rebrandly 6–10. Der Flaschenhals ist der Anbieter, nicht der Worker.

Pagining. Rebrandly verwendet einen last-Query-String-Parameter, der die ID des letzten Elements der vorherigen Seite übernimmt. Bitly gibt eine pagination.next-URL zurück. Beide sind Cursor-basiert; Rebrandly ist nur etwas gesprächiger. Die gesamte Schleife besteht aus sechs Zeilen:

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
}

Wir vertrauen dem Cursor. Wenn Rebrandly zweimal denselben last zurückgibt, würden wir in einer Endlosschleife hängen; das 30-Minuten-Budget begrenzt den Schaden.

Workspace-Scoping. Der API-Key von Rebrandly sieht jeden Link in jedem Workspace, dem der Benutzer angehört. Wenn Sie einen Agentur-Account mit fünf Kunden-Workspaces haben, möchten Sie fast sicher einen nach dem anderen importieren. Der Launcher macht dies als optionales Textfeld verfügbar – fügen Sie die Workspace-ID aus der URL-Leiste von Rebrandly ein oder lassen Sie es leer für „alles, was der Key sieht“.

Was wir nicht migrieren#

Klick-Historie. Rebrandlys Klickdaten sind nur im Premium-Tarif verfügbar und erscheinen als aggregierte Zähler pro Link, nicht als Klick-Ereignisse. Wir weisen auf jeder Oberfläche, die der Benutzer sieht, auf diese Grenze hin – die Dashboard-Rezeptseite, die /migrate-from/rebrandly-Landingpage, die Import-Fortschritts-UI und den FAQ-Bereich. Neue Klicks landen ab dem Zeitpunkt der Umstellung in der Elido-Analytik.

Rebrandly UTM-Vorlagen. Sie sind ein Feature zur Präsentationszeit in Rebrandly, das keine saubere API-Schnittstelle für den Export hat. Bauen Sie sie als Elido-Kampagnenregeln nach – der imported:rebrandly-Tag ist das Handle für die Massenzuweisung.

QR-Styling. Der Standard-Elido-QR wird für jeden importierten Link generiert; benutzerdefinierte Designs müssen neu angewendet werden. Die meisten Benutzer verwenden den Bulk-Tag-Filter, um nachträglich ein standardmäßiges Elido-CTA-Overlay oder eine Kampagne zuzuweisen.

Token-Handhabung#

Identisch zu Bitly. Das Token landet nie auf der Festplatte:

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

source_token_id bleibt NULL. ADR-0036s service_tokens-Tabelle ist für die Tier-2-Paste-Token-Integrationen (Mailchimp, Brevo, Klaviyo), bei denen die wiederkehrende Nutzung Persistenz rechtfertigt. Für Einmal-Migrationen ist „nur im Speicher“ der richtige operative Kompromiss – der Benutzer fügt das Token einmal ein, der Worker läuft, das Token ist weg.

context.WithoutCancel (Go 1.21+) behält die Werte des Kontextes bei – Logger, Trace-IDs, Deadline –, entfernt aber sein Abbruchsignal, sodass der Worker den HTTP-Request, der ihn gestartet hat, überlebt. Dies ist dasselbe Muster wie beim Bitly-Worker und dasselbe Muster, das jeder zukünftige Migrationsanbieter verwenden wird.

Konfliktlösung#

Drei Strategien, identisch zu Bitly. Der Benutzer wählt beim Starten des Jobs:

  • suffix (Standard): durchlaufe mylink-2, mylink-3, … bis zu 50 Kandidaten. Über 50 behandeln wir als strukturelles Problem und zeigen einen Fehler an.
  • skip: lass den bestehenden Elido-Link in Ruhe, protokolliere die Quellzeile, zähle als übersprungen.
  • fail: brich den gesamten Job beim ersten Konflikt ab. Für strikte 1:1-Semantik.

Die Slug-Suche ist ein indizierter Lesezugriff pro Zeile:

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
}

Wir bezahlen einen zusätzlichen Lesezugriff pro Zeile, erhalten aber einen deterministischen Suffix-Durchlauf und eine freundlichere Fehlermeldung. Die Alternative – nach einer Verletzung der Eindeutigkeit in pgx zu fischen und den Constraint-Namen aus der Fehlermeldung zu parsen – ist der schlechtere Kompromiss.

Was messbar ist#

Dieselbe strukturierten Zap-Logs wie bei Bitly. Workspace, Zieldomain, Konfliktstrategie, optionaler Workspace-Filter. Job-Lebenszyklus-Ereignisse – Start, Abschluss, Stuck-Sweep-Flips – sind bereits vorhanden und das Dashboard ruft alle zwei Sekunden den Polling-Endpunkt auf.

Wir grafen die Metriken der Migrationsjobs in Produktion noch nicht. Die Bitly-Kohorte gab uns unsere erste Baseline für echten Traffic; die Rebrandly-Daten sollten direkt vergleichbar sein, da der Worker mechanisch identisch ist und die Unterschiede in der Pagining-Form des Anbieters liegen. Erster Alert-Kandidat: Stuck-Sweep-Count > 0 in einem beliebigen einstündigen Fenster – das bedeutet, ein Worker ist gestorben und die UI des Benutzers hängt auf running.

Resumability (Wiederaufnahme) und das Deployment-Problem#

Derselbe Kompromiss wie bei Bitly. Der Worker läuft in-process; ein Deployment während des Imports beendet die Goroutine. Wir akzeptieren das für v1, weil:

  1. Die meisten Jobs sind in unter zehn Minuten fertig. Deployments sind zu Import-Zeiten eher selten.
  2. Das import_jobs.last_progress_at-Feld plus ein 5-Minuten-Stuck-Sweep-Cron setzt jede running-Zeile ohne Fortschritt in den letzten 30 Minuten auf failed mit einem klaren Grund.
  3. Das erneute Ausführen ist bei Suffix- und Skip-Strategien idempotent – bereits importierte Links werden beim zweiten Durchlauf erkannt und gemäß der Strategie aufgelöst.

Für Accounts mit mehr als 10.000 Links lohnt sich Resumability – wir zeichnen den Rebrandly last-Cursor in import_jobs.source_filter auf und machen da weiter, wo der letzte Lauf aufgehört hat. Das ist die nächste Iteration; die vier anderen Migrationsquellen werden von derselben Änderung profitieren, sobald wir sie bereitstellen.

Was kommt als Nächstes#

Dasselbe Gerüst, drei weitere Anbieter landen in derselben import_jobs-Tabelle.

  • Short.io — GET /links?limit=150&domain_id=…. Pro-Domain-Pagining; wir bitten den Benutzer, eine Quelldomain statt eines Workspaces zu wählen.
  • Dub.co — GET /api/links?projectSlug=…&limit=100. Ordner + Tags beibehalten; dies ist das sauberste der vier.
  • TinyURL — Pro/Bulk REST API. Kostenloses TinyURL hat keine API und hatte sie noch nie; dieser Pfad bleibt manuell.

Jeder landet hinter derselben Dashboard-Polling-UI und demselben imported:<vendor>-Tag-Muster. Der anbieterspezifische Worker bleibt in services/api-core/internal/imports/<vendor>.go.

Wenn Sie mit einem Rebrandly-Vergleich gezögert haben, weil der Migrationspfad nicht dokumentiert war, ist er jetzt dokumentiert. Probieren Sie es aus — vom API-Key bis zum letzten importierten Link in unter zehn Minuten für typische Accounts.

Verwandtes im Blog#

Elido testen

URL-Shortener mit EU-Hosting: eigene Domains, tiefe Analytik und eine offene API. Kostenloser Tarif — keine Kreditkarte nötig.

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

Weiterlesen