6 min czytaniaInżynieria

Wdrożenie migracji z Rebrandly: stronicowanie po 25 elementów i 30-minutowy budżet

Jak stworzyliśmy importowanie z Rebrandly jednym kliknięciem dla Elido — wolny rozmiar strony, UX filtra obszaru roboczego oraz to, czego celowo nie migrujemy.

Marius Voß
DevRel · edge infra
Diagram potoku: Rebrandly REST API po lewej stronie, przechodzące przez proces roboczy (worker) importu Elido do tabeli linków, z panelem bocznym zawierającym gwarancje liczbowe, które utrzymuje proces (limit 50 tys., budżet 30 min, 25 na stronę, token tylko w pamięci)

Drugie źródło migracji w naszym wdrożeniu Tier-3 zostało dzisiaj uruchomione. Wklej klucz API Rebrandly, opcjonalnie przefiltruj według obszaru roboczego, kliknij Start. Sześć do dziesięciu minut później każdy slashtag znajduje się w Twojej domenie Elido, z zachowanym slugiem, o ile nie wystąpiła kolizja. Migracja z Bitly, która pojawiła się dwa tygodnie temu, stworzyła fundamenty; Rebrandly to drugi dostawca, który z nich korzysta.

Ten wpis to opracowanie techniczne — co jest specyficzne dla Rebrandly, co zachowaliśmy identycznie jak w przypadku workera Bitly oraz gdzie API Rebrandly wymusiło inną strukturę.

Co jest wspólne z Bitly#

Cała funkcjonalność od początku miała opierać się na jednej tabeli i jednym kontrakcie workera. Oba założenia się sprawdziły.

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 zmienia się na rebrandly. source_filter zawiera {workspace_id: "..."}, gdy użytkownik filtruje; {}, gdy chce uzyskać każdy link widoczny dla danego klucza. Wszystko inne — 30-minutowy budżet, limit 50 tys. linków, strategia konfliktów typu suffix/skip/fail, tag imported:rebrandly — jest identyczne jak w przypadku ścieżki Bitly.

Launcher w panelu (apps/web/src/app/dashboard/integrations/[id]/rebrandly-migration-launcher.tsx) jest strukturalnie kopią launchera Bitly z usuniętą listą rozwijaną grup — Rebrandly posiada obszary robocze (workspaces), a nie grupy, więc udostępniamy je jako opcjonalny filtr tekstowy, zamiast wypełnianej listy rozwijanej, ponieważ endpoint Workspaces jest nieautoryzowany i stronicowany, a typowy użytkownik ma ich co najwyżej dwa.

W czym API Rebrandly się różni#

Trzy rzeczy:

Rozmiar strony. Rebrandly ogranicza pojedynczą stronę do 25 linków. Bitly do 100. Zatem konto z 5000 linków, którego migracja zajmuje 4–8 minut w Bitly, zajmuje 6–10 minut w Rebrandly. Wąskim gardłem jest dostawca, a nie worker.

Stronicowanie. Rebrandly używa parametru zapytania last, który przyjmuje identyfikator ostatniego elementu z poprzedniej strony. Bitly zwraca URL pagination.next. Oba są w stylu kursorów; ten z Rebrandly jest po prostu nieco bardziej gadatliwy. Cała pętla ma sześć linii:

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
}

Ufamy kursorowi. Jeśli Rebrandly zwróci ten sam identyfikator last dwa razy, zapętlimy się; 30-minutowy budżet ogranicza szkody.

Zakres obszarów roboczych. Klucz API Rebrandly widzi każdy link w każdym obszarze roboczym, do którego należy użytkownik. Jeśli posiadasz konto agencji z pięcioma obszarami roboczymi klientów, prawie na pewno chcesz importować je pojedynczo. Launcher udostępnia to jako opcjonalne pole tekstowe — wklej identyfikator obszaru roboczego z paska adresu URL Rebrandly lub pozostaw puste, aby zaimportować "wszystko, co widzi klucz".

Czego nie migrujemy#

Historia kliknięć. Dane o kliknięciach w Rebrandly są dostępne tylko w planie Premium i są prezentowane jako liczniki zagregowane na link, a nie jako zdarzenia pojedynczych kliknięć. Informujemy o tym ograniczeniu w każdym miejscu widocznym dla użytkownika — na stronie przepisu w panelu, na stronie docelowej /migrate-from/rebrandly, w interfejsie postępu importu oraz w sekcji FAQ. Nowe kliknięcia trafiają do analityki Elido od momentu przełączenia.

Szablony UTM w Rebrandly. Są one funkcją czasu prezentacji w Rebrandly i nie mają przejrzystego API do eksportu. Odbuduj je jako reguły kampanii Elido campaign rules — tag imported:rebrandly służy do masowego przypisywania.

Stylizacja kodów QR. Domyślny kod QR Elido jest generowany dla każdego zaimportowanego linku; niestandardowe projekty należy zastosować ponownie. Większość użytkowników używa masowego filtrowania po tagach, aby po fakcie przypisać domyślną nakładkę CTA Elido lub kampanię.

Obsługa tokenów#

Identyczna jak w Bitly. Token nigdy nie trafia na dysk:

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

source_token_id pozostaje NULL. Tabela service_tokens z ADR-0036 jest przeznaczona dla integracji Tier-2 z wklejanymi tokenami (Mailchimp, Brevo, Klaviyo), gdzie cykliczne użycie uzasadnia trwałe przechowywanie. W przypadku jednorazowych migracji, przechowywanie wyłącznie w pamięci jest właściwym kompromisem operacyjnym — użytkownik wkleja token raz, worker wykonuje zadanie, token znika.

context.WithoutCancel (Go 1.21+) zachowuje wartości kontekstu — logger, identyfikatory śledzenia, czas zakończenia — ale usuwa sygnał anulowania, dzięki czemu worker działa dłużej niż żądanie HTTP, które go uruchomiło. To ten sam wzorzec, co w przypadku workera Bitly, i ten sam wzorzec, którego użyje każdy przyszły dostawca migracji.

Rozwiązywanie konfliktów#

Trzy strategie, identyczne jak w Bitly. Użytkownik wybiera je podczas uruchamiania zadania:

  • suffix (domyślnie): przechodzenie przez mylink-2, mylink-3, … do 50 kandydatów. Po przekroczeniu 50 traktujemy to jako problem strukturalny i zgłaszamy błąd.
  • skip: pozostaw istniejący link Elido bez zmian, zaloguj wiersz źródłowy, policz jako pominięty.
  • fail: przerwij całe zadanie przy pierwszym konflikcie. Dla ścisłej semantyki 1:1.

Wyszukiwanie sluga to jeden odczyt indeksowany na wiersz:

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
}

Płacimy dodatkowym odczytem na wiersz, ale otrzymujemy deterministyczne przechodzenie przez sufiksy i bardziej przyjazny komunikat o błędzie. Alternatywa — szukanie naruszenia unikalności w pgx i parsowanie nazwy ograniczenia z ciągu błędu — jest gorszym rozwiązaniem.

Co jest mierzalne#

Te same ustrukturyzowane logi zap co w Bitly. Obszar roboczy, domena docelowa, strategia konfliktów, opcjonalny filtr obszaru roboczego. Zdarzenia cyklu życia zadania — start, zakończenie, wykrywanie zawieszonych zadań (stuck-sweep) — już istnieją, a panel sterowania odpytuje endpoint co dwie sekundy.

Nie tworzymy jeszcze wykresów metryk zadań migracyjnych na produkcji. Kohorta Bitly dała nam pierwszą bazę danych o rzeczywistym ruchu; dane z Rebrandly powinny być bezpośrednio porównywalne, ponieważ worker jest mechanicznie identyczny, a różnice dotyczą jedynie kształtu stronicowania dostawcy. Pierwszy kandydat do alertu: liczba zawieszonych zadań (stuck-sweep) > 0 w dowolnym oknie jednogodzinnym — oznacza to, że worker umarł, a interfejs użytkownika utknął w stanie running.

Wznawialność i problem wdrażania#

Ten sam kompromis co w Bitly. Worker działa wewnątrz procesu; wdrożenie w trakcie importu zabija gorutynę. Akceptujemy to w wersji v1, ponieważ:

  1. Większość zadań kończy się w mniej niż dziesięć minut. Wdrożenia są rzadkie w godzinach intensywnego importowania.
  2. Pole import_jobs.last_progress_at oraz 5-minutowy cron wykrywający zawieszone zadania zmieniają status każdego wiersza running bez postępu w ciągu ostatnich 30 minut na failed z jasnym powodem.
  3. Ponowne uruchomienie jest idempotentne w strategiach suffix i skip — już zaimportowane linki są wykrywane w drugim przebiegu i rozwiązywane zgodnie ze strategią.

W przypadku kont z ponad 10 000 linków, wznawialność staje się opłacalna — zapisujemy kursor last z Rebrandly w import_jobs.source_filter i wznawiamy pracę tam, gdzie ostatnie uruchomienie zostało przerwane. To kolejna iteracja; pozostałe cztery źródła migracji skorzystają z tej samej zmiany, gdy ją wdrożymy.

Co dalej#

Te same fundamenty, jeszcze trzech dostawców do wdrożenia w tej samej tabeli import_jobs.

  • Short.ioGET /links?limit=150&domain_id=…. Stronicowanie na domenę; prosimy użytkownika o wybranie domeny źródłowej zamiast obszaru roboczego.
  • Dub.coGET /api/links?projectSlug=…&limit=100. Folders + tags zachowane; to najczystsza implementacja z czterech.
  • TinyURL — Pro/Bulk REST API. Darmowy TinyURL nie posiada API i nigdy nie posiadał; ta ścieżka pozostaje ręczna.

Każdy z nich trafia za ten sam interfejs odpytywania w panelu sterowania i ten sam wzorzec tagu imported:<vendor>. Worker specyficzny dla dostawcy pozostaje w services/api-core/internal/imports/<vendor>.go.

Jeśli wstrzymywałeś się z porównaniem Rebrandly, ponieważ ścieżka migracji nie była udokumentowana, teraz już jest. Wypróbuj to — od klucza API do ostatniego zaimportowanego linku w mniej niż dziesięć minut w przypadku typowych kont.

Powiązane wpisy na blogu#

Wypróbuj Elido

Skracarka URL hostowana w UE: własne domeny, głęboka analityka i otwarte API. Darmowy plan — bez karty kredytowej.

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

Czytaj dalej