Skip to content

Fechar os gaps de analytics de conteúdo (vídeo + downloads)

Contexto

A auditoria de analytics (ver docs/FILE_DOWNLOAD_ANALYTICS.md) encontrou três gaps:

  1. A telemetria de playback de vídeo existe mas não coleta nada em produção. O backend está completo (apps.streamingMediaPlaybackSession/MediaPlaybackHeartbeat, POST /api/v1/media/<id>/heartbeat/, GET /api/v1/media/<id>/analytics/, ADR 0009) e o VideoPlayer (@repo/ui) embute o motor de telemetria (VideoAnalyticsConfig: analyticsUrl, mediaId, sessionId, heartbeatIntervalMs — heartbeats a cada 5s + pause/unmount). Porém nenhum chamador do SPA passa a prop analytics — aulas e produtos digitais renderizam o player sem telemetria. Os únicos dados de sessão existentes vêm do seed (_seed_gaps/media.py) e do admin do Django. Além disso, o detail de aula (CourseLessonDetailView) expõe video_url mas não o uid da mídia do vídeo principal, sem o qual o player não consegue apontar o heartbeat para a mídia correta.
  2. Permissões frouxas no resumo de analytics. MediaAnalyticsSummaryView exige apenas RequiresActiveMembership — qualquer membro ativo da comunidade vê total_views/watchtime/heatmap de qualquer mídia. Analytics é dado de administração.
  3. Downloads de arquivos não têm analytics nenhum. Drive (POST /api/v1/files/<id>/download/), anexos de aula (LessonAttachmentDownloadView) e produtos digitais (ProductItem.file via file_url presigned no detail) não registram download algum — nenhum modelo, contador ou endpoint de sumário. Pior: o download de produto digital não passa pelo backend (o frontend baixa direto do storage via <a href={file_url} download>), então nem seria possível registrar sem mudar o fluxo.

Decisão

Fechar os três gaps na mesma frente de trabalho "analytics de conteúdo":

1. Conectar a telemetria de vídeo de ponta a ponta no SPA

  • Backend: CourseLessonDetailView passa a expor video_media_id (uid do Media do vídeo principal) na resposta e no CourseLessonDetailSerializer (inclusive no stub locked). Produtos digitais já expõem o uid: ProductPackageItemVideoSerializer.id.
  • Frontend (apps/web): os players de aula (courses/$courseId/$lessonId.tsx) e de item de produto (products/$packageId/$itemId.tsx) passam {{ analyticsUrl: "/api/v1/media/<id>/heartbeat/", mediaId: <uid>, heartbeatIntervalMs: 5000 }} ao VideoPlayer. sessionId é opcional — o player usa o session_id retornado pelo servidor no primeiro heartbeat.
  • Regenerar o client Orval (make generate-api) após a mudança de schema.

2. Analytics é dado de administração

  • MediaAnalyticsSummaryView (e todos os novos endpoints de sumário de downloads) passam a exigir IsAdminOrModerator (de apps.spaces.permissions), além de RequiresCommunity/IsAuthenticated/RequiresActiveMembership. O endpoint de heartbeat permanece permissivo (authentication_classes = [] + RequiresCommunity) — ele é o ponto de ingestão do player e não expõe agregados.

3. Analytics de downloads em app dedicado apps.downloads

  • Modelo: FileDownloadEvent (estende BaseModel) com GenericForeignKey para DriveFile | Media | ProductItem + community, membership (nullable), ip_address, user_agent, referrer, snapshots de content_type/file_size. Um evento por download (download é evento discreto; sem contador denormalizado — derivável por agregação).
  • Registro: nos pontos onde o backend já gera a URL — DriveFileViewSet.download e LessonAttachmentDownloadView; novo ProductItemDownloadView (GET /api/headless/v1/products/{package_id}/items/{item_id}/download) valida acesso → registra → 302 para presigned URL (mesmo padrão de anexos de aula). O frontend de produto troca o link direto por esse endpoint.
  • Sumários: endpoint genérico GET /api/v1/downloads/analytics/?object_type=drive|media|product&object_id=<uid> (total, 30d, únicos, timeline, top baixadores) + seção files no AdminAnalyticsView (dashboard admin). Gate IsAdminOrModerator.
  • Detalhes de schema/endpoints/UI: docs/FILE_DOWNLOAD_ANALYTICS.md (design completo, fases de implementação).

Decisões provisionais

As perguntas em aberto da seção 8 de docs/FILE_DOWNLOAD_ANALYTICS.md foram resolvidas por padrão neste ADR: endpoint de sumário genérico (object_type+object_id), file_url mantido para preview inline (TablatureViewer), contagem de downloads somente no admin (sem badge público). Revisitar se o produto exigir exposição pública.

Consequências

  • O pipeline de vídeo passa a coletar dados reais em produção (fonte: apps.streaming), tornando o seed e o admin meros instrumentos de inspeção.
  • Analytics de playback e downloads ficam com gate uniforme (admin/moderador) — corrige a exposição atual do MediaAnalyticsSummaryView.
  • Downloads passam a ser auditáveis e mensuráveis (quem baixou o quê, quando), com o produto digital deixando de contornar o backend.
  • apps.downloads cresce como o app natural para futuras métricas de arquivos (visualizações/previews) sem inchar apps.media/apps.spaces.
  • Volume: FileDownloadEvent é de cardinalidade baixa-média por tenant; sem necessidade de worker/agregação separada no primeiro momento.
  • Mudanças de API (novo campo video_media_id, novo endpoint de download de produto, novos endpoints de analytics) exigem make generate-api — client gerado é o contrato (regra Orval).

Strum — Documentação.