Logo
Créer un compte
← Tous les articles

Suivre les succès depuis un backend Java

Mike Dalton Par Mike Dalton ·

Un panneau d'éditeur de code et les logos Java et Spring à 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-java-example-app, un petit service Spring Boot sur Java 21 qui reçoit un déblocage depuis le jeu et le transmet à Game Stats AI en utilisant le RestClient de Spring et Jackson. 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

application.yml contient l'hôte de l'API et lit l'id de compte et le token depuis des variables d'environnement :

gamestats:
  base-url: https://gamestats.ai
  account-id: ${GAMESTATS_ACCOUNT_ID:}
  token: ${GAMESTATS_TOKEN:}

Le token est un secret, et lire l'id de compte depuis l'environnement de la même façon garde les deux hors du dépôt. Définissez les variables GAMESTATS_ACCOUNT_ID et GAMESTATS_TOKEN partout où l'application s'exécute :

export GAMESTATS_ACCOUNT_ID=your_account_id
export GAMESTATS_TOKEN=server_your_token_here

Utiliser des variables d'environnement dans chaque environnement garde le développement et la production identiques. Si vous préférez un fichier pour le développement local, placez les valeurs dans un application-local.yml ignoré par git et démarrez l'application avec SPRING_PROFILES_ACTIVE=local.

Un record lié au préfixe gamestats contient les trois valeurs :

package ai.gamestats.example;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.bind.DefaultValue;

@ConfigurationProperties("gamestats")
public record GameStatsProperties(
    @DefaultValue("https://gamestats.ai") String baseUrl,
    String accountId,
    String token) {
}

É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. Un record avec des annotations @JsonProperty fait correspondre les noms Java aux noms en snake_case que l'API attend :

package ai.gamestats.example;

import com.fasterxml.jackson.annotation.JsonProperty;
import java.time.Instant;

public record AchievementEvent(
    @JsonProperty("version_name") String versionName,
    @JsonProperty("player_username") String playerUsername,
    @JsonProperty("achievement_name") String achievementName,
    @JsonProperty("occurred_at") Instant occurredAt) {
}

Vous pouvez supprimer les annotations et définir plutôt la stratégie de nommage SNAKE_CASE de Jackson ; les annotations gardent le format attendu par l'API visible à un seul endroit.

occurred_at est un horodatage ISO 8601. Jackson sérialise un Instant dans ce format avec un Z final, donc Instant.now() 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 RestClient

Spring Boot configure automatiquement un RestClient.Builder qui porte déjà la configuration JSON et HTTP du framework. Injectez-le dans le client et définissez l'URL de base et le token bearer une seule fois, dans le constructeur :

public GameStatsClient(RestClient.Builder restClientBuilder, GameStatsProperties properties) {
    this.restClient = restClientBuilder
        .baseUrl(properties.baseUrl())
        .defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + properties.token())
        .build();
    this.accountId = properties.accountId();
}

Un RestClient construit est immuable et thread-safe, donc cette unique instance partagée gère chaque requête. Si votre application n'utilise pas l'injection de dépendances, appelez RestClient.builder() vous-même et conservez le résultat dans un champ à longue durée de vie.

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

Le client envoie le record en POST et inspecte le résultat :

package ai.gamestats.example;

import java.util.List;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestClient;

@Component
public class GameStatsClient {

    private final RestClient restClient;
    private final String accountId;

    public GameStatsClient(RestClient.Builder restClientBuilder, GameStatsProperties properties) {
        this.restClient = restClientBuilder
            .baseUrl(properties.baseUrl())
            .defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + properties.token())
            .build();
        this.accountId = properties.accountId();
    }

    public void sendAchievement(AchievementEvent achievementEvent) {
        try {
            restClient.post()
                .uri("/api/v1/accounts/{accountId}/achievement_events", accountId)
                .body(achievementEvent)
                .retrieve()
                .toBodilessEntity();
        } catch (HttpClientErrorException.UnprocessableEntity exception) {
            ValidationErrors errors = exception.getResponseBodyAs(ValidationErrors.class);
            throw new IllegalStateException(String.join("; ", errors == null ? List.of() : errors.errors()));
        }
    }

    public record ValidationErrors(List<String> errors) {
    }
}

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 le message de l'exception. Un token incorrect ou absent renvoie 401 ou 403 sans corps, que retrieve() transforme en HttpClientErrorException.

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

Partout où votre backend apprend un déblocage, injectez GameStatsClient et envoyez l'événement. L'application d'exemple le fait dans un contrôleur que le client du jeu appelle :

package ai.gamestats.example;

import java.time.Instant;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class AchievementController {

    private final GameStatsClient gameStatsClient;

    public AchievementController(GameStatsClient gameStatsClient) {
        this.gameStatsClient = gameStatsClient;
    }

    @PostMapping("/players/{username}/achievements")
    @ResponseStatus(HttpStatus.ACCEPTED)
    public void unlock(@PathVariable String username, @RequestBody UnlockRequest request) {
        gameStatsClient.sendAchievement(new AchievementEvent(
            request.versionName(),
            username,
            request.achievementName(),
            Instant.now()));
    }

    public record UnlockRequest(String achievementName, String versionName) {
    }
}

UnlockRequest est un record à deux champs avec le nom du succès et le nom de version envoyés par le client du jeu.

Envoyer l'événement en ligne est le chemin minimal. En option, ajoutez @Retryable de Spring Retry pour des réessais automatiques, ce qui est sûr puisque renvoyer un déblocage n'a aucun effet, ou déplacez les envois hors du chemin de la requête avec @Async ou une file d'attente en arrière-plan.

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

© 2026 Rowhome Labs, LLC