feat(gherkin): ajouter features API et UI pour audio-guides multi-séquences
Créer 5 nouvelles features API : - creation-gestion.feature : création, modification, publication d'audio-guides - declenchement-gps.feature : calculs GPS, rayons, déclenchement automatique - progression-sync.feature : sauvegarde progression, sync cloud, multi-device - publicites.feature : insertion pub, fréquence, métriques créateur - metriques-analytics.feature : statistiques, heatmaps, analytics créateur Adapter mode-voiture.feature : - ajouter section publicités en mode voiture (auto-play, pas de pause) Corriger .gitignore : - remplacer "api" par "/api" pour ne pas ignorer features/api/
This commit is contained in:
485
features/api/audio-guides/metriques-analytics.feature
Normal file
485
features/api/audio-guides/metriques-analytics.feature
Normal file
@@ -0,0 +1,485 @@
|
||||
# language: fr
|
||||
|
||||
Fonctionnalité: API - Métriques et analytics audio-guides
|
||||
En tant que système backend
|
||||
Je veux collecter et exposer les métriques d'écoute des audio-guides
|
||||
Afin de fournir des insights aux créateurs
|
||||
|
||||
Contexte:
|
||||
Étant donné que l'API RoadWave est démarrée
|
||||
Et que le créateur "creator@example.com" est authentifié
|
||||
|
||||
# Statistiques globales audio-guide
|
||||
|
||||
Scénario: GET /api/v1/creators/me/audio-guides/{id}/stats - Statistiques générales
|
||||
Étant donné un audio-guide "ag_123" avec les métriques suivantes:
|
||||
| ecoutes_totales | ecoutes_completes | taux_completion | temps_ecoute_total |
|
||||
| 1542 | 892 | 58% | 423h |
|
||||
Quand je fais un GET sur "/api/v1/creators/me/audio-guides/ag_123/stats"
|
||||
Alors le code HTTP de réponse est 200
|
||||
Et le corps de réponse contient:
|
||||
"""json
|
||||
{
|
||||
"listens_total": 1542,
|
||||
"listens_complete": 892,
|
||||
"completion_rate": 58,
|
||||
"total_listen_time_seconds": 1522800,
|
||||
"avg_listen_time_seconds": 988,
|
||||
"unique_listeners": 1124,
|
||||
"repeat_listeners": 418
|
||||
}
|
||||
"""
|
||||
|
||||
Scénario: Statistiques par période (7j, 30j, 90j, all-time)
|
||||
Étant donné un audio-guide avec historique
|
||||
Quand je fais un GET sur "/api/v1/creators/me/audio-guides/ag_123/stats?period=7d"
|
||||
Alors le code HTTP de réponse est 200
|
||||
Et les statistiques sur les 7 derniers jours sont retournées
|
||||
|
||||
Plan du Scénario: Périodes disponibles
|
||||
Quand je fais un GET avec period=<period>
|
||||
Alors les stats de la période <description> sont retournées
|
||||
|
||||
Exemples:
|
||||
| period | description |
|
||||
| 7d | 7 derniers jours |
|
||||
| 30d | 30 derniers jours |
|
||||
| 90d | 90 derniers jours |
|
||||
| all | Depuis la création |
|
||||
|
||||
# Métriques par séquence
|
||||
|
||||
Scénario: GET /api/v1/creators/me/audio-guides/{id}/sequences/stats - Stats par séquence
|
||||
Étant donné un audio-guide de 12 séquences
|
||||
Et les métriques suivantes:
|
||||
| sequence | starts | completions | abandon_rate |
|
||||
| 1 | 1000 | 950 | 5% |
|
||||
| 2 | 950 | 920 | 3% |
|
||||
| 3 | 920 | 850 | 8% |
|
||||
Quand je fais un GET sur "/api/v1/creators/me/audio-guides/ag_123/sequences/stats"
|
||||
Alors le code HTTP de réponse est 200
|
||||
Et le corps de réponse contient:
|
||||
"""json
|
||||
{
|
||||
"sequences": [
|
||||
{
|
||||
"sequence_id": "seq_1",
|
||||
"sequence_number": 1,
|
||||
"title": "Introduction",
|
||||
"starts": 1000,
|
||||
"completions": 950,
|
||||
"completion_rate": 95,
|
||||
"abandon_rate": 5,
|
||||
"avg_listen_time": 132,
|
||||
"duration": 135
|
||||
},
|
||||
{
|
||||
"sequence_id": "seq_2",
|
||||
"sequence_number": 2,
|
||||
"title": "Pyramide du Louvre",
|
||||
"starts": 950,
|
||||
"completions": 920,
|
||||
"completion_rate": 97,
|
||||
"abandon_rate": 3,
|
||||
"avg_listen_time": 106,
|
||||
"duration": 108
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
Scénario: Identification séquence la plus écoutée
|
||||
Étant donné les statistiques par séquence
|
||||
Quand je fais un GET sur "/api/v1/creators/me/audio-guides/ag_123/sequences/top"
|
||||
Alors le code HTTP de réponse est 200
|
||||
Et le corps de réponse contient:
|
||||
"""json
|
||||
{
|
||||
"most_listened": {
|
||||
"sequence_id": "seq_3",
|
||||
"title": "La Joconde",
|
||||
"starts": 920,
|
||||
"reason": "popular_highlight"
|
||||
},
|
||||
"least_listened": {
|
||||
"sequence_id": "seq_11",
|
||||
"title": "Aile Richelieu",
|
||||
"starts": 580,
|
||||
"reason": "late_sequence"
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
# Points d'abandon
|
||||
|
||||
Scénario: Détection point d'abandon critique
|
||||
Étant donné un audio-guide avec taux de complétion 58%
|
||||
Et que 35% des utilisateurs abandonnent à la séquence 7
|
||||
Quand je fais un GET sur "/api/v1/creators/me/audio-guides/ag_123/abandon-analysis"
|
||||
Alors le code HTTP de réponse est 200
|
||||
Et le corps de réponse contient:
|
||||
"""json
|
||||
{
|
||||
"critical_abandon_point": {
|
||||
"sequence_id": "seq_7",
|
||||
"sequence_number": 7,
|
||||
"title": "Aile Richelieu",
|
||||
"abandon_rate": 35,
|
||||
"severity": "high",
|
||||
"suggestion": "Réduire la durée (8 min actuellement) ou rendre plus captivant"
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
Scénario: Heatmap des abandons
|
||||
Étant donné un audio-guide
|
||||
Quand je fais un GET sur "/api/v1/creators/me/audio-guides/ag_123/abandon-heatmap"
|
||||
Alors le code HTTP de réponse est 200
|
||||
Et le corps de réponse contient une heatmap:
|
||||
"""json
|
||||
{
|
||||
"heatmap": [
|
||||
{"sequence": 1, "abandon_count": 50, "intensity": "low"},
|
||||
{"sequence": 2, "abandon_count": 30, "intensity": "low"},
|
||||
{"sequence": 7, "abandon_count": 320, "intensity": "high"},
|
||||
{"sequence": 12, "abandon_count": 70, "intensity": "medium"}
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
# Métriques géographiques
|
||||
|
||||
Scénario: GET /api/v1/creators/me/audio-guides/{id}/geographic-stats - Stats géo
|
||||
Étant donné un audio-guide géolocalisé
|
||||
Et les écoutes suivantes par région:
|
||||
| region | listens | completions |
|
||||
| Île-de-France | 850 | 520 |
|
||||
| PACA | 320 | 180 |
|
||||
| Auvergne | 145 | 90 |
|
||||
Quand je fais un GET sur "/api/v1/creators/me/audio-guides/ag_123/geographic-stats"
|
||||
Alors le code HTTP de réponse est 200
|
||||
Et le corps de réponse contient:
|
||||
"""json
|
||||
{
|
||||
"by_region": [
|
||||
{
|
||||
"region": "Île-de-France",
|
||||
"listens": 850,
|
||||
"completions": 520,
|
||||
"completion_rate": 61
|
||||
},
|
||||
{
|
||||
"region": "PACA",
|
||||
"listens": 320,
|
||||
"completions": 180,
|
||||
"completion_rate": 56
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
Scénario: Heatmap géographique des écoutes
|
||||
Étant donné un audio-guide avec points GPS
|
||||
Quand je fais un GET sur "/api/v1/creators/me/audio-guides/ag_123/geographic-heatmap"
|
||||
Alors le code HTTP de réponse est 200
|
||||
Et le corps de réponse contient:
|
||||
"""json
|
||||
{
|
||||
"points": [
|
||||
{
|
||||
"sequence_id": "seq_1",
|
||||
"gps_point": {"lat": 43.1234, "lon": 2.5678},
|
||||
"listen_count": 1000,
|
||||
"density": "high"
|
||||
},
|
||||
{
|
||||
"sequence_id": "seq_2",
|
||||
"gps_point": {"lat": 43.1245, "lon": 2.5690},
|
||||
"listen_count": 950,
|
||||
"density": "high"
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
# Métriques déclenchement GPS
|
||||
|
||||
Scénario: Attribution GPS auto vs manuel
|
||||
Étant donné un audio-guide voiture avec 8 points GPS
|
||||
Et les déclenchements suivants:
|
||||
| type | count |
|
||||
| GPS auto | 542 |
|
||||
| Manuel | 123 |
|
||||
| Point manqué | 89 |
|
||||
Quand je fais un GET sur "/api/v1/creators/me/audio-guides/ag_123/trigger-stats"
|
||||
Alors le code HTTP de réponse est 200
|
||||
Et le corps de réponse contient:
|
||||
"""json
|
||||
{
|
||||
"total_triggers": 754,
|
||||
"by_type": {
|
||||
"gps_auto": 542,
|
||||
"manual": 123,
|
||||
"missed_point": 89
|
||||
},
|
||||
"gps_auto_rate": 72,
|
||||
"manual_rate": 16,
|
||||
"missed_rate": 12
|
||||
}
|
||||
"""
|
||||
|
||||
Scénario: Points GPS les plus manqués
|
||||
Étant donné les statistiques de points manqués
|
||||
Quand je fais un GET sur "/api/v1/creators/me/audio-guides/ag_123/missed-points"
|
||||
Alors le code HTTP de réponse est 200
|
||||
Et le corps de réponse contient:
|
||||
"""json
|
||||
{
|
||||
"most_missed": [
|
||||
{
|
||||
"sequence_id": "seq_5",
|
||||
"title": "Enclos des éléphants",
|
||||
"missed_count": 45,
|
||||
"missed_rate": 12,
|
||||
"suggestion": "Rayon trop petit (30m) ou point mal placé"
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
# Temps moyen par séquence
|
||||
|
||||
Scénario: Comparaison durée audio vs temps d'écoute moyen
|
||||
Étant donné les métriques temporelles suivantes:
|
||||
| sequence | duration | avg_listen_time | ecart |
|
||||
| 1 | 135 | 130 | -5s |
|
||||
| 2 | 108 | 90 | -18s |
|
||||
| 3 | 222 | 220 | -2s |
|
||||
Quand je fais un GET sur "/api/v1/creators/me/audio-guides/ag_123/time-analysis"
|
||||
Alors le code HTTP de réponse est 200
|
||||
Et le corps de réponse contient:
|
||||
"""json
|
||||
{
|
||||
"sequences": [
|
||||
{
|
||||
"sequence_id": "seq_1",
|
||||
"duration_seconds": 135,
|
||||
"avg_listen_time": 130,
|
||||
"delta": -5,
|
||||
"completion_avg": 96
|
||||
},
|
||||
{
|
||||
"sequence_id": "seq_2",
|
||||
"duration_seconds": 108,
|
||||
"avg_listen_time": 90,
|
||||
"delta": -18,
|
||||
"completion_avg": 83,
|
||||
"warning": "Séquence souvent skippée ou abandonnée avant la fin"
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
# Notifications milestones
|
||||
|
||||
Scénario: POST /api/v1/audio-guides/{id}/milestones/check - Vérification milestone
|
||||
Étant donné qu'un audio-guide atteint 1000 écoutes
|
||||
Quand le système vérifie les milestones
|
||||
Alors un événement "milestone_reached" est émis
|
||||
Et une notification est envoyée au créateur:
|
||||
"""json
|
||||
{
|
||||
"type": "milestone",
|
||||
"milestone_type": "listens_1000",
|
||||
"audio_guide_id": "ag_123",
|
||||
"message": "Félicitations ! Votre audio-guide 'Visite du Louvre' a atteint 1000 écoutes !",
|
||||
"stats": {
|
||||
"listens": 1000,
|
||||
"completion_rate": 58
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
Plan du Scénario: Milestones prédéfinis
|
||||
Étant donné qu'un audio-guide atteint <seuil> écoutes
|
||||
Quand le milestone est vérifié
|
||||
Alors une notification "<type>" est envoyée
|
||||
|
||||
Exemples:
|
||||
| seuil | type |
|
||||
| 100 | listens_100 |
|
||||
| 500 | listens_500 |
|
||||
| 1000 | listens_1000 |
|
||||
| 5000 | listens_5000 |
|
||||
| 10000 | listens_10000 |
|
||||
|
||||
# Graphiques et visualisations
|
||||
|
||||
Scénario: GET /api/v1/creators/me/audio-guides/{id}/completion-funnel - Entonnoir complétion
|
||||
Étant donné un audio-guide de 12 séquences
|
||||
Quand je fais un GET sur "/api/v1/creators/me/audio-guides/ag_123/completion-funnel"
|
||||
Alors le code HTTP de réponse est 200
|
||||
Et le corps de réponse contient un graphique en entonnoir:
|
||||
"""json
|
||||
{
|
||||
"funnel": [
|
||||
{"sequence": 1, "listeners": 1000, "percentage": 100},
|
||||
{"sequence": 2, "listeners": 950, "percentage": 95},
|
||||
{"sequence": 3, "listeners": 890, "percentage": 89},
|
||||
{"sequence": 12, "listeners": 580, "percentage": 58}
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
Scénario: GET /api/v1/creators/me/audio-guides/{id}/listens-over-time - Écoutes dans le temps
|
||||
Étant donné un audio-guide avec historique
|
||||
Quand je fais un GET sur "/api/v1/creators/me/audio-guides/ag_123/listens-over-time?period=30d"
|
||||
Alors le code HTTP de réponse est 200
|
||||
Et le corps de réponse contient une série temporelle:
|
||||
"""json
|
||||
{
|
||||
"period": "30d",
|
||||
"granularity": "day",
|
||||
"data_points": [
|
||||
{"date": "2026-01-01", "listens": 45, "completions": 28},
|
||||
{"date": "2026-01-02", "listens": 52, "completions": 31},
|
||||
{"date": "2026-01-03", "listens": 38, "completions": 24}
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
# Comparaisons et benchmarks
|
||||
|
||||
Scénario: GET /api/v1/creators/me/audio-guides/compare - Comparaison audio-guides
|
||||
Étant donné que le créateur a 3 audio-guides:
|
||||
| audio_guide_id | title | listens | completion_rate |
|
||||
| ag_1 | Tour de Paris | 1200 | 65% |
|
||||
| ag_2 | Visite Louvre | 1542 | 58% |
|
||||
| ag_3 | Safari du Paugre | 890 | 72% |
|
||||
Quand je fais un GET sur "/api/v1/creators/me/audio-guides/compare"
|
||||
Alors le code HTTP de réponse est 200
|
||||
Et le corps de réponse contient:
|
||||
"""json
|
||||
{
|
||||
"audio_guides": [
|
||||
{
|
||||
"audio_guide_id": "ag_2",
|
||||
"title": "Visite Louvre",
|
||||
"listens": 1542,
|
||||
"completion_rate": 58,
|
||||
"rank_by_listens": 1,
|
||||
"rank_by_completion": 2
|
||||
},
|
||||
{
|
||||
"audio_guide_id": "ag_1",
|
||||
"title": "Tour de Paris",
|
||||
"listens": 1200,
|
||||
"completion_rate": 65,
|
||||
"rank_by_listens": 2,
|
||||
"rank_by_completion": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
Scénario: Benchmark par rapport à la moyenne plateforme
|
||||
Étant donné qu'un audio-guide a un taux de complétion de 58%
|
||||
Et que la moyenne plateforme pour la catégorie "musée" est 62%
|
||||
Quand je fais un GET sur "/api/v1/creators/me/audio-guides/ag_123/benchmark"
|
||||
Alors le code HTTP de réponse est 200
|
||||
Et le corps de réponse contient:
|
||||
"""json
|
||||
{
|
||||
"your_completion_rate": 58,
|
||||
"category_avg": 62,
|
||||
"platform_avg": 60,
|
||||
"performance": "below_category_avg",
|
||||
"percentile": 45
|
||||
}
|
||||
"""
|
||||
|
||||
# Événements trackés
|
||||
|
||||
Scénario: POST /api/v1/events/track - Tracking événements utilisateur
|
||||
Étant donné qu'un utilisateur interagit avec un audio-guide
|
||||
Quand un événement se produit
|
||||
Alors il est tracké avec les données suivantes:
|
||||
| événement | données |
|
||||
| audio_guide_started | audio_guide_id, mode, user_id |
|
||||
| sequence_completed | sequence_id, completion_rate, duration |
|
||||
| audio_guide_completed | audio_guide_id, total_time, sequences_count|
|
||||
| point_gps_triggered | point_id, distance, auto_or_manual |
|
||||
| point_gps_missed | point_id, distance, action_taken |
|
||||
|
||||
Scénario: Exemple événement audio_guide_started
|
||||
Quand un audio-guide démarre
|
||||
Alors l'événement suivant est envoyé:
|
||||
"""json
|
||||
{
|
||||
"event_type": "audio_guide_started",
|
||||
"timestamp": "2026-01-22T14:00:00Z",
|
||||
"user_id": "user_456",
|
||||
"audio_guide_id": "ag_123",
|
||||
"mode": "voiture",
|
||||
"device": "ios",
|
||||
"location": {"lat": 43.1234, "lon": 2.5678}
|
||||
}
|
||||
"""
|
||||
|
||||
Scénario: Exemple événement point_gps_triggered
|
||||
Quand un point GPS déclenche une séquence
|
||||
Alors l'événement suivant est envoyé:
|
||||
"""json
|
||||
{
|
||||
"event_type": "point_gps_triggered",
|
||||
"timestamp": "2026-01-22T14:05:30Z",
|
||||
"user_id": "user_456",
|
||||
"audio_guide_id": "ag_123",
|
||||
"sequence_id": "seq_2",
|
||||
"trigger_type": "gps_auto",
|
||||
"distance_to_point": 25,
|
||||
"speed_kmh": 28
|
||||
}
|
||||
"""
|
||||
|
||||
# Export données
|
||||
|
||||
Scénario: GET /api/v1/creators/me/audio-guides/{id}/export - Export CSV
|
||||
Étant donné un audio-guide avec historique complet
|
||||
Quand je fais un GET sur "/api/v1/creators/me/audio-guides/ag_123/export?format=csv"
|
||||
Alors le code HTTP de réponse est 200
|
||||
Et le Content-Type est "text/csv"
|
||||
Et le fichier CSV contient:
|
||||
| user_id | sequence_id | started_at | completed_at | completion_rate |
|
||||
| user_123 | seq_1 | 2026-01-22 14:10:00 | 2026-01-22 14:12:15 | 100 |
|
||||
| user_123 | seq_2 | 2026-01-22 14:12:20 | 2026-01-22 14:14:08 | 100 |
|
||||
|
||||
Scénario: Export JSON pour analyse externe
|
||||
Quand je fais un GET sur "/api/v1/creators/me/audio-guides/ag_123/export?format=json"
|
||||
Alors le code HTTP de réponse est 200
|
||||
Et le Content-Type est "application/json"
|
||||
Et le fichier JSON contient toutes les métriques détaillées
|
||||
|
||||
# Cache et performance
|
||||
|
||||
Scénario: Cache Redis pour stats fréquemment consultées
|
||||
Étant donné que les stats globales d'un audio-guide sont en cache Redis
|
||||
Quand je fais un GET sur "/api/v1/creators/me/audio-guides/ag_123/stats"
|
||||
Alors les stats sont récupérées depuis Redis (pas PostgreSQL)
|
||||
Et le temps de réponse est < 50ms
|
||||
Et le cache a un TTL de 5 minutes
|
||||
|
||||
Scénario: Invalidation cache lors de nouvelles écoutes
|
||||
Étant donné que les stats sont en cache Redis
|
||||
Quand une nouvelle écoute complète est enregistrée
|
||||
Alors le cache Redis est invalidé pour cet audio-guide
|
||||
Et le prochain GET recalcule les stats depuis PostgreSQL
|
||||
|
||||
Scénario: Pré-calcul stats quotidien (job batch)
|
||||
Étant donné que le job batch s'exécute chaque nuit à 3h
|
||||
Quand le job démarre
|
||||
Alors pour chaque audio-guide actif:
|
||||
- Les stats sont calculées depuis les événements bruts
|
||||
- Les résultats sont stockés dans audio_guide_stats_daily
|
||||
- Les agrégations (7j, 30j, 90j) sont pré-calculées
|
||||
Et les requêtes du lendemain sont instantanées (lecture table pré-calculée)
|
||||
Reference in New Issue
Block a user