Firebase Authentication の匿名ユーザーを削除する

Firebase Authentication で匿名ユーザーを使う場合, プランに関わらずユーザーアカウントの上限は1億までになっている。

Firebase Authentication の上限


つまり1億PV以上になると新たに匿名ユーザーを作れず破滅する可能性があるので, 定期的にユーザーの削除が必要になってくる。


めちゃめちゃバズらないと1億PVはそうそう行かなそうだけど, 可能性はゼロではないので対策しておいたほうがよさそう。


匿名ユーザーの削除は Firebase 側では自動でやってくれたりはしないので、自動でスクリプトなどを用意する必要がある。


今回は golang で削除スクリプトを実装した。

package main

import (
    "context"
    "fmt"
    "log"
    "os"
    "time"

    firebase "firebase.google.com/go/v4"
    "firebase.google.com/go/v4/auth"
    "google.golang.org/api/iterator"
)

const maxDeletableUsersCount = 1000

func main() {
    ctx := context.Background()

    app, err := firebase.NewApp(ctx, nil)
    if err != nil {
        log.Fatalf("error new firebase app: %s\n", err)
    }

    auth, err := app.Auth(ctx)
    if err != nil {
        log.Fatalf("error create auth client: %s\n", err)
    }

    users, err := getInactiveAnonymousUsers(ctx, auth)
    if err != nil {
        log.Fatalf("error get users: %s\n", err)
    }

    fmt.Printf("%v個の匿名ユーザーアカウントが削除されます。よろしいですか? [y/n]:", len(users))

    var answer string
    fmt.Scan(&answer)

    if answer != "y" && answer != "Y" {
        os.Exit(1)
    }

    successCount, failureCount, err := deleteUsers(ctx, auth, users)
    if err != nil {
        log.Fatalf("error delete users, error:%s, success:%v, failure:%v\n", err, successCount, failureCount)
    }

    fmt.Printf("delete user complete, success:%v, failure:%v\n", successCount, failureCount)
}

func getInactiveAnonymousUsers(ctx context.Context, auth *auth.Client) ([]string, error) {
    var users []string

    iter := auth.Users(ctx, "")
    for {
        user, err := iter.Next()
        if err == iterator.Done {
            break
        }

        if err != nil {
            return nil, err
        }

        // 匿名ユーザーではない場合削除しない
                 // 匿名ユーザーしかいない場合は問答無用で削除してしまってよさそう
        if len(user.ProviderUserInfo) > 0 {
            continue
        }

        // LastLogInTimeStamp は ミリ秒 までを含めた値が返ってくるのに対し
        // time.Unix()は 秒 までを含めた値が返ってくるので, 比較できるように丸める
        // https://firebase.google.com/docs/reference/admin/java/reference/com/google/firebase/auth/UserMetadata#public-long-getlastsignintimestamp
        lastLogInTimestamp := user.UserMetadata.LastLogInTimestamp / time.Second.Milliseconds()
        // 1ヶ月以上ログインしていないユーザーを削除対象にする
        if lastLogInTimestamp < time.Now().AddDate(0, -1, 0).Unix() {
            users = append(users, user.UID)
            fmt.Printf("deletable user, uid:%s, last login:%v \n", user.UID, time.Unix(lastLogInTimestamp, 0))
        }
    }

    return users, nil
}

func deleteUsers(ctx context.Context, auth *auth.Client, uIDs []string) (successCount int, failureCount int, err error) {
    successCount = 0
    failureCount = 0

    // auth.DeleteUsers で削除できるユーザーは1000件までなので, スライスを1000件に分割して繰り返し実行する
    for len(uIDs) > maxDeletableUsersCount {
        slice := uIDs[0:maxDeletableUsersCount]
        uIDs = uIDs[maxDeletableUsersCount:]
        result, err := auth.DeleteUsers(ctx, slice)
        if err != nil {
            return successCount, failureCount, err
        }
        successCount += result.SuccessCount
        failureCount += result.FailureCount
    }

    result, err := auth.DeleteUsers(ctx, uIDs)
    if err != nil {
        return successCount, failureCount, err
    }
    successCount += result.SuccessCount
    failureCount += result.FailureCount

    return successCount, failureCount, nil
}

あとはこれを cron 等で定期的に実行してあげれば安心。