Logo
Créer un compte
← Tous les articles

Suivre les succès depuis un backend Go

Mike Dalton Par Mike Dalton ·

Un panneau d'éditeur de code et le logo Go à côté du logo Game Stats AI, avec une flèche pointant vers une carte de tableau de bord montrant un trophée et un graphique à barres.

Le suivi des succès enregistre les jalons que les joueurs atteignent dans votre jeu : terminer un niveau, trouver un objet caché, vaincre un boss. Ces données montrent jusqu'où vont les joueurs, où ils s'arrêtent et quel contenu ils ne trouvent jamais.

Si votre jeu communique déjà avec un backend que vous contrôlez, ce backend est un bon endroit d'où envoyer les événements de succès. Votre token d'API reste sur vos serveurs au lieu d'être embarqué dans le client du jeu, et l'intégration fonctionne de la même façon quel que soit le moteur utilisé côté client.

Pour montrer à quoi cela ressemble, nous avons construit game-stats-ai-go-example-app, un petit service HTTP sur Go 1.22 qui reçoit un déblocage depuis le jeu et le transmet à Game Stats AI en utilisant uniquement net/http et encoding/json de la bibliothèque standard. Ce guide le déroule étape par étape.

Étape 1 : créer un token serveur

Game Stats AI propose deux types de tokens d'API. Les tokens client ne peuvent qu'envoyer de la télémétrie, ils peuvent donc être embarqués sans risque dans un client de jeu. Les tokens serveur peuvent envoyer de la télémétrie et lire les rapports, et ils doivent rester sur un backend de confiance.

Comme ce code s'exécute sur votre backend, créez un token serveur sur gamestats.ai/api_tokens. La même page affiche l'id de compte dont l'URL de l'endpoint a besoin.

Le formulaire de nouveau token d'API avec le nom rempli et le type de token Server sélectionné.

Étape 2 : stocker le token et l'id de compte dans la configuration

L'application lit l'hôte de l'API, l'id de compte et le token depuis des variables d'environnement, donc aucun secret ne vit dans le dépôt. Définissez l'id de compte et le token partout où l'application s'exécute :

export GAMESTATS_ACCOUNT_ID=your_account_id
export GAMESTATS_TOKEN=server_your_token_here

Un petit struct contient les trois valeurs, lues depuis l'environnement. GAMESTATS_BASE_URL est optionnel et vaut par défaut l'hôte de production :

package main

import "os"

type Config struct {
    BaseURL   string
    AccountID string
    Token     string
}

func LoadConfig() Config {
    baseURL := os.Getenv("GAMESTATS_BASE_URL")
    if baseURL == "" {
        baseURL = "https://gamestats.ai"
    }

    return Config{
        BaseURL:   baseURL,
        AccountID: os.Getenv("GAMESTATS_ACCOUNT_ID"),
        Token:     os.Getenv("GAMESTATS_TOKEN"),
    }
}

Lire chaque valeur depuis l'environnement garde le développement et la production identiques. Si vous préférez un fichier pour le développement local, gardez les export dans un fichier ignoré par git et chargez-le avec source avant de démarrer l'application.

Étape 3 : définir le payload de l'événement

Game Stats AI accepte les événements de succès sur un seul endpoint :

POST https://gamestats.ai/api/v1/accounts/{account_id}/achievement_events
Authorization: Bearer <token>
Content-Type: application/json

Le corps comporte quatre champs obligatoires. Les tags de struct font correspondre les noms des champs Go aux noms en snake_case que l'API attend :

package main

import "time"

type AchievementEvent struct {
    VersionName     string    `json:"version_name"`
    PlayerUsername  string    `json:"player_username"`
    AchievementName string    `json:"achievement_name"`
    OccurredAt      time.Time `json:"occurred_at"`
}

La bibliothèque standard n'a pas de stratégie de nommage globale, donc chaque champ déclare son nom sur le réseau dans son tag.

occurred_at est un horodatage ISO 8601. encoding/json sérialise un time.Time au format RFC 3339, et une valeur UTC se termine par un Z final, donc time.Now().UTC() n'a besoin d'aucune chaîne de format.

Les succès, les joueurs et les versions sont créés la première fois qu'ils apparaissent dans un événement, il n'y a donc rien à enregistrer à l'avance. Renvoyer le même succès pour le même joueur n'a aucun effet, ce qui rend les nouvelles tentatives sûres.

Étape 4 : configurer un client HTTP

Un http.Client de la bibliothèque standard est sûr pour un usage concurrent, alors construisez-en un et réutilisez-le pour chaque requête. Un petit struct le conserve avec l'URL de base, le token et l'id de compte :

func NewGameStatsClient(config Config) *GameStatsClient {
    return &GameStatsClient{
        httpClient: &http.Client{Timeout: 10 * time.Second},
        baseURL:    config.BaseURL,
        token:      config.Token,
        accountID:  config.AccountID,
    }
}

Un seul client partagé gère chaque requête, et le délai d'attente limite le temps qu'un envoi attend avant d'abandonner.

Étape 5 : envoyer l'événement et gérer la réponse

Le client sérialise l'événement, l'envoie en POST et inspecte le résultat :

package main

import (
    "bytes"
    "context"
    "encoding/json"
    "fmt"
    "net/http"
    "strings"
    "time"
)

type GameStatsClient struct {
    httpClient *http.Client
    baseURL    string
    token      string
    accountID  string
}

func NewGameStatsClient(config Config) *GameStatsClient {
    return &GameStatsClient{
        httpClient: &http.Client{Timeout: 10 * time.Second},
        baseURL:    config.BaseURL,
        token:      config.Token,
        accountID:  config.AccountID,
    }
}

type validationErrors struct {
    Errors []string `json:"errors"`
}

func (client *GameStatsClient) SendAchievement(ctx context.Context, event AchievementEvent) error {
    body, err := json.Marshal(event)
    if err != nil {
        return err
    }

    url := fmt.Sprintf("%s/api/v1/accounts/%s/achievement_events", client.baseURL, client.accountID)
    request, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
    if err != nil {
        return err
    }
    request.Header.Set("Authorization", "Bearer "+client.token)
    request.Header.Set("Content-Type", "application/json")

    response, err := client.httpClient.Do(request)
    if err != nil {
        return err
    }
    defer response.Body.Close()

    if response.StatusCode == http.StatusUnprocessableEntity {
        var validation validationErrors
        if err := json.NewDecoder(response.Body).Decode(&validation); err != nil {
            return err
        }
        return fmt.Errorf("game stats rejected the event: %s", strings.Join(validation.Errors, "; "))
    }

    if response.StatusCode >= http.StatusMultipleChoices {
        return fmt.Errorf("game stats returned status %d", response.StatusCode)
    }

    return nil
}

Un événement mis en file d'attente renvoie 201 Created avec un objet JSON vide. Un payload qui échoue à la validation renvoie 422 Unprocessable Entity avec les raisons, comme {"errors":["Achievement name can't be blank"]}, et le client les expose dans l'erreur renvoyée. Un token incorrect ou absent renvoie 401 ou 403 sans corps, que le client signale comme un code de statut inattendu.

Étape 6 : l'appeler quand un joueur débloque un succès

Partout où votre backend apprend un déblocage, appelez le client et envoyez l'événement. L'application d'exemple le fait depuis un handler HTTP que le client du jeu appelle :

package main

import (
    "encoding/json"
    "log"
    "net/http"
    "time"
)

type unlockRequest struct {
    AchievementName string `json:"achievementName"`
    VersionName     string `json:"versionName"`
}

func main() {
    client := NewGameStatsClient(LoadConfig())

    mux := http.NewServeMux()
    mux.HandleFunc("POST /players/{username}/achievements", func(writer http.ResponseWriter, request *http.Request) {
        var unlock unlockRequest
        if err := json.NewDecoder(request.Body).Decode(&unlock); err != nil {
            http.Error(writer, err.Error(), http.StatusBadRequest)
            return
        }

        event := AchievementEvent{
            VersionName:     unlock.VersionName,
            PlayerUsername:  request.PathValue("username"),
            AchievementName: unlock.AchievementName,
            OccurredAt:      time.Now().UTC(),
        }

        if err := client.SendAchievement(request.Context(), event); err != nil {
            http.Error(writer, err.Error(), http.StatusBadGateway)
            return
        }

        writer.WriteHeader(http.StatusAccepted)
    })

    log.Println("listening on :8080")
    log.Fatal(http.ListenAndServe(":8080", mux))
}

unlockRequest est un struct à deux champs avec le nom du succès et le nom de version envoyés par le client du jeu. Le routeur net/http de Go 1.22 fait correspondre la méthode et le segment de chemin {username}, et request.PathValue le relit.

Envoyer l'événement en ligne est le chemin minimal. En option, réessayez un envoi échoué, ce qui est sûr puisque renvoyer un déblocage n'a aucun effet, ou déplacez l'envoi hors du chemin de la requête vers une goroutine en arrière-plan ou une file d'attente.

Le projet complet se trouve dans game-stats-ai-go-example-app.

© 2026 Rowhome Labs, LLC