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:
- Die meisten Jobs sind in unter zehn Minuten fertig. Deployments sind zu Import-Zeiten eher selten.
- Das
import_jobs.last_progress_at-Feld plus ein 5-Minuten-Stuck-Sweep-Cron setzt jederunning-Zeile ohne Fortschritt in den letzten 30 Minuten auffailedmit einem klaren Grund. - 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.