🧮

Debug da Ordenação por Idioma

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 opcional
debugRanking=true. Para um administrador com sessão iniciada
(adminRole: "admin"), em qualquer ambiente, a resposta traz:

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 de
GET /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