Debug da ordenação por idioma (`debugRanking`)
Para validar a ordenação por idioma das sugestões sem refazer as contas à mão.
Os endpoints ordenados por idioma aceitam o parâmetro opcionaldebugRanking=true. Para um administrador com sessão iniciada
(adminRole: "admin"), em qualquer ambiente, a resposta traz:
- em cada item servido, um objeto
_rankingcom as parcelas da sua pontuação; - as preferências de idioma resolvidas e a força aplicada: no cabeçalho
X-Ranking-Debug(JSON) de todas as respostas e, nas respostas que são um
objeto, também no campo_rankingDebug.
Para qualquer outro pedido (convidado, utilizador sem papel de administrador,viewer, ou só a flag antiga isAdmin) um valor válido (true ou false) é
ignorado em silêncio: a resposta é igual à que teria sem ele, sem erro. Um
valor inválido (ex: debugRanking=yes) é um 400 para todos, admins
incluídos, como qualquer parâmetro inválido. A app nunca o envia (os testes de
contrato confirmam-no).
As respostas com detalhes levam Cache-Control: private, no-store (nenhuma
cache as guarda) e o cabeçalho X-Ranking-Debug está nos exposedHeaders do
CORS, para o admin portal o poder ler no browser. Nos pedidos deGET /recommendations e GET /recommendations/weekly-discovery de um admin com
a flag, o RecommendationLoggingInterceptor não regista o pedido, nem
impressões, nem supressões: não é um utilizador a ser servido (os handlers
marcam o pedido). Nas outras rotas de /recommendations* (similar,schedules, magic-button...) a flag não tem efeito e o pedido regista-se como
sempre, admins incluídos.
Só mostra os dados de quem faz o pedido: não há forma de pedir a ordenação de
outro utilizador.
Sem a flag (o normal), as respostas são exatamente as de sempre e não há
nenhuma leitura extra à base de dados.
Endpoints
| Endpoint | Força | Notas |
|---|---|---|
GET /home/highlights |
homeHighlightsLanguageStrength() (1) |
popular_radios e popular_podcasts. A posição conta dentro do grupo do pool (país ou variante), que vem em _ranking.group. Calculado sobre o mesmo pool em cache que é servido. |
GET /radios?sortBy=home ("Ver mais") |
homeHighlightsLanguageStrength() (1) |
Com a lista de sempre (sem idiomas, força 0 ou filtro country), cada item traz section: "not-ranked" e bónus 0. |
GET /radios/nearby |
suggestionLanguageStrength() (0,5) |
Ordenado por idioma sobre a ordem das candidatas, cada parte por popularidade. Numa app antiga que só envia language (sem país), as candidatas são as mais populares dessa língua (até 250, nunca as mais recentes) e, se não chegarem, as restantes mais recentes. Sem país e sem sessão não há idiomas, por isso _ranking traz strength 0 e positionScore pela ordem servida. |
GET /radios/nearby-geo |
suggestionLanguageStrength() (0,5) |
Ordenado por idioma sobre a ordem por precisão das coordenadas (pontos próprios por distância intercalados 1:1 com pontos de cidade por popularidade, raio a raio). Só as primeiras 100 dessa ordem são ordenadas por idioma (section: "window"); as seguintes mantêm essa ordem, sem bónus (section: "beyond-window"). No fallback haversine toda a lista é ordenada (section: "haversine-fallback"). Fora da janela, e sem idiomas, positionScore depende da página pedida. |
GET /home/new-episodes |
suggestionLanguageStrength() (0,5) |
Os favoritos vêm primeiro, por data, sem bónus (section: "favourites"); os restantes são ordenados dentro da sua lista ("languages", depois o complemento "global"). |
GET /recommendations |
suggestionLanguageStrength() (0,5) |
Ordenado por pedido sobre os itens em cache (Redis): os detalhes são os da ordem servida. Uma leitura das referências de idioma, como sem a flag. |
GET /recommendations/weekly-discovery |
suggestionLanguageStrength() (0,5) |
Limitação: a descoberta é guardada quando é gerada e a sua ordem veio dos candidatos desse momento (fundidos por posição relativa e com mínimos por variante), que não são guardados. _ranking é recalculado sobre a ordem guardada com os idiomas atuais (section: "stored"): a pontuação não tem de descer ao longo da lista. Só aqui há uma leitura extra (as referências de idioma dos itens). |
Os valores da força são os de omissão (SUGGESTION_LANGUAGE_STRENGTH=0.5).
`_ranking`
| Campo | Significado |
|---|---|
locale / language |
A referência que a afinidade usou: o locale do item quando é válido; senão a língua (comparada pela língua base). Nenhum: nada a comparar. |
affinity |
languageAffinity: 1 (variante preferida), 0,6 (outra variante da lista), 0,25 (mesma língua, outro país), 0. |
positionScore |
Pontuação da posição na lista que a ordenação recebeu: (n - i) / n (1 para o primeiro), ou dentro do grupo nos destaques. |
languageBonus |
strength × affinity. |
score |
positionScore + languageBonus: a ordem é por score, do maior para o menor, mantendo a ordem original nos empates. |
strength |
A força aplicada. 0 quando a ordenação por idioma não correu (sem preferências, força desligada, secções sem bónus). |
match |
Como a afinidade foi decidida: variant pelo locale do item (1 a variante preferida, 0,6 outra da lista, 0,25 a mesma língua noutro país); language só pela língua, o item ainda sem locale (1 a língua da variante preferida, 0,6 a de outra da lista); none sem correspondência (afinidade 0). |
preferred |
affinity 1: a variante preferida, ou só a sua língua quando match é language. |
listed |
affinity 0,6: outra variante da lista, ou só a sua língua quando match é language. |
section |
Opcional: a parte da lista em que o item foi ordenado, quando há várias (ver a tabela acima). |
group |
Só nos destaques: o grupo do pool (país das rádios, variante ou língua dos podcasts). |
Numa lista ordenada só por pontuação (uma section), score nunca sobe de um
item para o seguinte: é a forma de validar a ordem.
Resumo (`X-Ranking-Debug` / `_rankingDebug`)
{
"prefs": { "preferred": "en-GB", "others": ["pt-PT"], "source": "user" },
"strength": 1,
"note": "..."
}
source: user (a lista guardada do utilizador), user-default (utilizador
sem lista: o idioma por omissão do seu país), device (convidado: idioma e
país do dispositivo), none (sem preferências; strength é então 0).
Exemplo
TOKEN='<access token de um administrador>'
curl -s -D - \
-H "Authorization: Bearer $TOKEN" \
'https://<api>/api/radios?sortBy=home&page=1&pageSize=5&debugRanking=true' \
| tee /dev/stderr | sed -n '/^{/,$p' | jq '.items[] | {name, _ranking}'
{
"name": "BBC Radio 1",
"_ranking": {
"locale": "en-GB",
"affinity": 1,
"positionScore": 1,
"languageBonus": 1,
"score": 2,
"strength": 1,
"preferred": true,
"listed": false
}
}
Com um token que não é de administrador, ou sem token, o mesmo pedido devolve
a resposta de sempre, sem _ranking nem X-Ranking-Debug.
Implementação
explainLanguageRanking(content-features/src/lib/locale/language-ranking-explain.ts)
reproduz as contas dererankByLanguageitem a item; os testes garantem que
a ordem é a mesma.explainHomePoolRanking(worker-shared) faz o mesmo
pararankHomePoolByLanguage.- O acesso:
rankingDebugAllowed(apps/api/src/app/common/ranking-debug/ranking-debug.ts). - Nos DTOs de query (
/radios,/radios/nearby,/radios/nearby-geo) o
parâmetro é@DebugRankingQuery(); nos endpoints que leem a query campo a
campo (/home/*,/recommendations*),@DebugRankingFlag(), que lê o valor
em bruto: um@Query('debugRanking')booleano não serve, porque oValidationPipeglobal transforma qualquer valor que não seja'true'emfalseeyespassaria em silêncio. ranking-debug-http.int-spec.tstesta tudo isto por HTTP (app real,configureApp, MongoDB 8.0 em memória): flag ignorada para convidado,
utilizador eviewer(mesmo corpo, sem cabeçalho), detalhes eno-store
para o admin, 400 parayes,Varye o cabeçalho exposto no CORS.