Tier-3ロールアウトにおける5つ目、そして最後の移行元となる機能が本日リリースされました。Dub.co APIトークンを貼り付け、必要に応じてプロジェクトスラッグでフィルタリングし、「開始」をクリックするだけです。3〜5分後には、すべてのリンクがスラッグを保持したままElidoドメイン上に移行され、Dubのフォルダ構造はElidoのタグへとフラット化されます。
本稿はエンジニアリングの記録です。Dub特有の仕様、5つのサポートベンダーの中でDubのAPIが最もクリーンである理由、そしてDubからElidoへのサイドグレードを検討すべき理由について解説します。
なぜこの移行機能が必要なのか#
Dub.coは、機能面においてElidoに最も近いオープンソース相当のプロダクトです。強力な製品力、クリーンなREST API、モダンなダッシュボードを備えています。この移行パスは、以下の3つの理由のいずれかでElidoを選択するチームのために用意されました。
- EUでのデータレジデンシー。 ElidoのデータプレーンはEUに固定されています(Hetzner FRA + ASH POPs、APAC向けにはOVH SGPを使用)。すべてのクリックイベントはデフォルトでEUリージョンのClickHouseに記録されます。Dub Cloudは米国拠点であり、GDPR/Schrems-II postureがトレードオフとなります。
- エッジPOPのフットプリント。 p95 < 15ms cache HITを実現する3つのリージョンPOPは、DubのCloudflare-Workersのみのパスとは異なるレイテンシターゲットを持っています。レイテンシに敏感なワークロード(モバイルファースト、広告アトリビューション)では、その違いを実感できるはずです。
- 分析の深さ。 ClickHouseを基盤としたクリック分析により、イベントごとのリテンション、GA4/Meta CAPIへのコンバージョン転送、完全な履歴再生が可能です。Dubの分析機能もクリーンですが、PostgreSQLによる集計であり、深さの上限が異なります。
これらの要件が当てはまらない場合、Dubは優れた製品です。この移行機能は、これらの要件を必要とするチームのために提供されます。
DubのAPIが最もクリーンである理由#
私たちはこれまでに5つのベンダーAPIに対してワーカーを構築してきました。統合の容易さ順に並べると以下の通りです。
- Dub.co — ベアラートークン、JSON-RFC準拠のエラー、
?page=+?limit=100によるページネーション、すべてのフィールドにペイロード例が記載されたドキュメント。 - Short.io — クリーンで
HasMoreブール値が明示されていますが、ドメインごとのパーティショニングにはUXの改善の余地があります。 - Bitly —
pagination.nextURLは標準に準拠しており、関連するAPI referenceは網羅的です。 - TinyURL — Pro/Bulkのみがサポート対象で、それ以外は非対応です。ドキュメントも乏しいです。
- Rebrandly —
?last=<id>カーソルは問題ありませんが、1ページあたり25件という上限により、すべてが遅く感じられます。
Dubの強み:ドキュメントにはcurlの例が含まれ、エラーレスポンスにはマシンコードと人間が読めるメッセージの両方が含まれています。また、ページネーションは?page=2&limit=100が予測通りに動作する、非常に標準的なものです。
const dubPageSize = 100
page := 1
for {
resp, err := w.fetchPage(ctx, opts.Token, opts.WorkspaceID, page)
if err != nil { /* mark failed */ return }
if len(resp) == 0 { break }
for _, link := range resp { /* import */ }
if len(resp) < dubPageSize { break }
page++
}
DubはHasMoreフラグを返さないため、ページの件数が少ないことから終了を推測します。これは標準的なRESTページネーションのパターンであり、適切に機能します。つまり、制限よりも少ないページを受け取った時点で終了です。
フォルダからタグへのフラット化#
Dubには整理用プリミティブとしてフォルダとタグの両方がありますが、Elidoにはタグしかありません。そのため、移行時にDubのフォルダはタグとしてフラット化されます。
tags := make([]string, 0, len(link.Tags)+2)
for _, t := range link.Tags {
tags = append(tags, t.Name)
}
if link.Folder != nil && link.Folder.Name != "" {
// Dub folders can nest; flatten the full path.
for _, segment := range strings.Split(link.Folder.Name, "/") {
seg := strings.TrimSpace(segment)
if seg != "" {
tags = append(tags, seg)
}
}
}
tags = append(tags, "imported:dub")
フォルダcampaigns/q3-launchにあり、タグpaidとlinkedinを持つDubリンクは、paid、linkedin、campaigns、q3-launch、imported:dubというタグでインポートされます。Elidoのフィルタセマンティクスは、DubのフォルダUIと同じ取得パターン(タグ一致、タグ部分一致、複数タグのAND検索)を処理します。サーバーサイドでフォルダ階層を再構築するのではなく、ユーザーにはフラットなタグリストとフィルタプリミティブを提供します。
Elidoにフォルダを追加できたでしょうか?可能です。しかし、Elidoのデータモデルがフェーズ1で出荷されたとき、タグのみを採用しました。フォルダはデスクトップファイルシステムのメンタルモデルには適していますが、ショートリンクの一括操作には適していません。DubユーザーをElidoのタグへ移行することは、そのトレードオフとして正しい選択です。
プロジェクトフィルタリング#
Dubは(新しいUIでは)「ワークスペース」を使用し、以前はそれらを「プロジェクト」と呼んでいました。APIはフィルタリング用にworkspaceIdパラメータを受け入れます。ランチャーでは、これをオプションのテキストフィールドとして公開しています。DubのURLからワークスペーススラッグを貼り付けるか、空白のままにしてトークンが参照できるすべてのリンクを取得します。
これはRebrandlyのワークスペースフィルタやShort.ioのドメインフィールドと同じです。5つの移行ベンダーのうち3つがアカウントごとのパーティショニング概念を持っています。私たちは、ドロップダウンよりも一貫してオプションのテキスト入力として公開しています。なぜなら、一般的なユーザーは最大でも2つのワークスペースしか持たず、API listing endpointを呼び出すことで発生するレイテンシに見合う価値はないと判断したためです。
移行対象外のもの#
Dubのジオターゲティングおよびデバイスターゲティングルール。これらは強力なDubの機能ですが、ルール構成がElidoのスマートリンクルールと1:1でマッピングされません。スラッグはインポートされます。Elidoの式構文を使用してルールを再構築してください。これはより柔軟ですが、異なるメンタルモデルが必要です。
クリックごとの履歴。これは5つすべての移行元に共通する制限です。Dubのクリックごとのデータは分析エンドポイントの裏側にあり、プランの階層に縛られています。切り替え後の新しいクリックはElidoの分析機能に記録されます。
QRコードのデザイン。デフォルトのElido QRが再生成されます。カスタムデザインは再適用が必要です。imported:dubタグは、一括再割り当てのハンドルとして機能します。
DubワークスペースのACLとロール設定。ElidoではSCIM/SSOまたはワークスペースメンバーの招待を使用してアクセス権を再付与してください。両製品間でロールモデルが大きく異なるため、自動マッピングは予期せぬ権限昇格につながるリスクがあります。
セルフホスト型Dub#
Dubはオープンソースであり、セルフホスト型のDubインスタンスも一般的です。移行機能はDub Cloudが公開しているのと同じREST APIを使用しているため、セルフホスト型Dubをターゲットにする場合はDUB_API_BASEをオーバーライドすることで対応可能です。v1ではこれをセルフサービスの公開設定としませんでした。運用のコストが無視できないためです。異なるDubのバージョンは微妙に異なるレスポンス形式を返し、テスト済みのターゲットであるv0.9に対して、Dub v0.7環境でサイレントに500エラーを返すようなランチャーをリリースしたくありません。
セルフホスト型Dubからの移行については、[email protected]までDubのバージョンを記載の上メールをお送りください。個別にコンシェルジュ移行を実行します。実環境で十分なバージョンを確認でき次第、オーバーライド設定をダッシュボードのセルフサービス設定に追加します。
トークンの取り扱い#
他の4つの移行ベンダーと同じワンショットセマンティクスを採用しています。
bgCtx := context.WithoutCancel(r.Context())
go h.dub.Run(bgCtx, job.ID, imports.DubJobOptions{
Token: req.Token,
WorkspaceID: req.WorkspaceID,
})
source_token_idはNULLのままです。トークンはワーカーの実行中のみapi-coreプロセスのメモリに保持され、完了後に破棄されます。永続化は行いません。これは繰り返し呼び出されるベンダー連携ではなく、ワンショットの移行処理であるためです。
context.WithoutCancel(Go 1.21以降)は、HTTPリクエストの終了後もワーカーを維持します。これは、このロールアウトにおける他のすべての移行ベンダーと同じパターンです。
競合解決とワーカーの契約#
他のすべてのベンダーと同一です。mylink-2、mylink-3、…と最大50個の候補までサフィックスを試行します。既存のElidoリンクはスキップされ、最初の競合で終了するのではなく、スキップされます。ワーカーの契約(MaxLinksPerImport=50_000、ImportRunBudget=30*time.Minute)は、5つすべてで共有されています。
検索は(domain_id, slug)に対する行ごとのインデックス付き読み取りとなります。pgxで一意制約違反をトラップするよりも、決定論的なサフィックス探索の方がエラーハンドリングが容易です。
Bitlyとの比較#
DubとBitlyはどちらもほぼ同じスループットで移行されます(1ページあたり100リンク、安定して毎秒約5件のインサート)。5,000リンクのソースであれば、どちらのベンダーでも4〜7分で完了します。ユーザーから見える違いは、インポート後の体験です。Dubからインポートされたリンクは、タグとしてフラット化されたフォルダのパンくずリスト付きで到着します。Bitlyからインポートされたリンクは、imported:bitlyタグとBitlyの自由記述タグが付与された状態で到着します。
再開可能性とデプロイの問題#
最初の4つの移行と同様のトレードオフです。ワーカーはプロセス内で動作するため、インポート途中のデプロイはゴルーチンを終了させます。5分ごとのスタックスイープCronジョブが、30分間進捗のないrunningステータス行をfailedに切り替えます。再実行はサフィックスとスキップ戦略により、冪等性が保証されます。
10,000リンクを超えるアカウントでは、再開可能性は重要です。Dubのpageカーソルをimport_jobs.source_filterに記録し、最後に完了したページから再開します。5つの移行ベンダーすべてが同じインプロセス設計を共有しているため、再開機能を実装すれば、5つすべてが恩恵を受けられます。
Tier-3ロールアウトの次#
Tier-3は完了です。5つの移行ベンダー、1つの共有import_jobsテーブル、1つの共有ワーカー契約、1つの共有ダッシュボードポーリングUI、5つのSEOランディングページ、5つのエンジニアリングブログ投稿。
Tier-3の次に控えているもの:
- 10,000リンク超のアカウント向けの再開可能性。ベンダーごとのカーソルチェックポインティング。
- トークンが失効したプランのユーザー向けのCSVエクスポートによる代替手段。現在はコンシェルジュのみ。
- Tier-2 service_tokensの基盤 — Mailchimp、Brevo、Klaviyo向けの繰り返し利用可能なベンダートークン。移行パスでJSONB
source_filterパターンを検証しました。Tier-2には永続的な暗号化トークンが必要であり、これはADR-0036の領域です。
DubのワークスペースからElidoに注目していた方は、移行のストーリーがドキュメント化されました。試してみてください。一般的なアカウントであれば、トークン入力から最後のリンクインポートまで5分以内に完了します。
ブログの関連トピック#
- Shipping the Bitly migration: a worker, a token, a 30-minute budget
- Shipping the Rebrandly migration: 25-per-page pagination
- Shipping the Short.io migration: per-domain pagination at 150/page
- Shipping the TinyURL migration: Pro/Bulk REST, no free-tier path
- Short links as Terraform: the engineering cornerstone