Premier commit du cinquième front. Trois parties : - `unity/MyInfoMateVR/` : le projet Unity (6000.0.83f1, URP, Meta XR SDK 205), un APK unique pour tous les clients. Menu flottant à sélection au regard, appairage, chargement de scène GLB, POI, cache de contenu, télémétrie. - `unity-overlay/` : les mêmes scripts à recopier sur un projet Unity neuf, avec les pièges rencontrés consignés dans son README. - `viewer/` : viewer et éditeur de scène web autonome (Vite, TypeScript, three.js), partagé avec les autres fronts. - `docs/` : état des lieux, setup Unity, décisions d'architecture et plan d'exécution en 9 étapes. La scène est décrite par un `scene.json` poussé par `adb push` : l'app le préfère à celui embarqué dans l'APK. Les binaires (GLB, textures de l'échantillon Sponza, DLL Meta XR) passent par Git LFS dès ce premier commit — les y faire entrer après coup demanderait de réécrire l'historique. Les artefacts régénérés par l'éditeur et par CMake (`Library/`, `.utmp/`, Burst debug) sont ignorés. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
89 lines
3.7 KiB
C#
89 lines
3.7 KiB
C#
using System;
|
|
using UnityEngine;
|
|
|
|
namespace MyInfoMate.Vr.Net
|
|
{
|
|
/// <summary>
|
|
/// Télémétrie de visite — item <b>E9</b> du lot XR-4.
|
|
///
|
|
/// <b>Les stats du manager fonctionnent sans une ligne de plus côté serveur</b> :
|
|
/// <c>VisitEvent</c> porte déjà un <c>AppType</c>, et l'écran Statistiques filtre
|
|
/// génériquement sur <c>AppType.values</c> — le canal VR y apparaîtra tout seul au
|
|
/// premier événement reçu.
|
|
///
|
|
/// ⚠️ Deux différences avec le reste de l'API, vérifiées dans <c>StatsController</c> :
|
|
/// la route est <c>POST /api/stats/event</c> (pas <c>/api/visitevent</c>), et
|
|
/// <c>appType</c> comme <c>eventType</c> partent en <b>chaîne</b>, parsés par nom
|
|
/// côté serveur. Un nom inconnu d'<c>eventType</c> est un 400 ; un <c>appType</c>
|
|
/// inconnu retombe <b>silencieusement sur Mobile</b> — c'est-à-dire qu'une faute de
|
|
/// frappe ici ferait compter les visites du casque dans le canal mobile.
|
|
///
|
|
/// L'envoi est « tire et oublie » : une borne ne doit jamais attendre une statistique,
|
|
/// et un événement perdu ne vaut pas un message d'erreur au visiteur.
|
|
/// </summary>
|
|
public class Telemetry
|
|
{
|
|
/// <summary>Le nom, pas la valeur : le serveur parse par nom (<c>Enum.TryParse</c>).</summary>
|
|
const string AppTypeVr = "VR";
|
|
|
|
readonly ApiClient _client;
|
|
readonly string _instanceId;
|
|
readonly string _configurationId;
|
|
readonly string _language;
|
|
|
|
/// <summary>
|
|
/// Une session = une visite. Sur une borne, elle se renouvelle à chaque retour au
|
|
/// menu ou à la repose du casque (E10), pas au lancement de l'app : sinon une
|
|
/// journée entière de borne compterait pour un seul visiteur.
|
|
/// </summary>
|
|
public string SessionId { get; private set; } = NewSessionId();
|
|
|
|
public Telemetry(ApiClient client, string instanceId, string configurationId, string language)
|
|
{
|
|
_client = client;
|
|
_instanceId = instanceId;
|
|
_configurationId = configurationId;
|
|
_language = language;
|
|
}
|
|
|
|
public void StartNewSession() => SessionId = NewSessionId();
|
|
|
|
public void SectionView(string sectionId) => Send("SectionView", sectionId);
|
|
|
|
public void SectionLeave(string sectionId, int seconds) =>
|
|
Send("SectionLeave", sectionId, seconds);
|
|
|
|
public void MenuItemTap(string sectionId) => Send("MenuItemTap", sectionId);
|
|
|
|
public void MapPoiTap(string sectionId, string poiTitle) =>
|
|
Send("MapPoiTap", sectionId, metadata: $"{{\"poi\":\"{Escape(poiTitle)}\"}}");
|
|
|
|
async void Send(string eventType, string sectionId,
|
|
int? durationSeconds = null, string metadata = null)
|
|
{
|
|
var result = await _client.PostAsync<object>("/api/stats/event", new
|
|
{
|
|
instanceId = _instanceId,
|
|
configurationId = _configurationId,
|
|
sectionId,
|
|
sessionId = SessionId,
|
|
eventType,
|
|
appType = AppTypeVr,
|
|
language = _language,
|
|
durationSeconds,
|
|
metadata,
|
|
timestamp = DateTime.UtcNow
|
|
});
|
|
|
|
// La réponse est un 204 sans corps : la désérialisation ne dit rien d'utile,
|
|
// seul un échec réseau mérite une trace — et rien ne remonte au visiteur.
|
|
if (!result.Ok) Debug.Log($"[Stats] {eventType} non envoyé : {result.Error}");
|
|
}
|
|
|
|
static string NewSessionId() => Guid.NewGuid().ToString("N");
|
|
|
|
static string Escape(string value) =>
|
|
string.IsNullOrEmpty(value) ? "" : value.Replace("\\", "\\\\").Replace("\"", "\\\"");
|
|
}
|
|
}
|