Pular para o conteúdo
~/.primo-academy.sh
☰ Aulas · Observabilidade Frontend: Sentry, RUM, OpenTelemetry, feature flags e PII · 0/7
Recomendado: essencial

Real User Monitoring: Web Vitals no client, custom metrics, session replay

4 min de leitura

fonte

Voce tem error tracking (Sentry captura exceptions). Mas e' metade: o user pode estar frustrado sem nenhum erro - LCP de 8s, INP de 1s, CLS de 0.5 (pagina pulando). Sem erro, Sentry nao ve. Esse no cobre Real User Monitoring (RUM): medir Web Vitals (LCP, INP, CLS) de users reais (nao synthetic), enviar pra backend de analytics, e usar session replay pra reproduzir o que o user viu.

RUM complementa error tracking: error tracking = "algo quebrou", RUM = "o user teve boa experiencia?". Em 2026, Core Web Vitals (LCP, INP, CLS) sao ranking factor do Google

  • performance ruim = SEO ruim. RUM e' obrigatorio em apps que querem rankeiar.

Voce sai de "achismo" pra "data de performance real por segmento (device, pais, conexao)".

O essencial 🟢

Core Web Vitals (CWV) - o que cada um mede.

  • LCP (Largest Contentful Paint): quando o maior elemento visivel renderizou. Percepcao: "a pagina carregou?". Bom: < 2.5s.
  • INP (Interaction to Next Paint): latencia de interacao ate a proxima pintura. Percepcao: "cliquei e a UI respondeu?". Bom: < 200ms.
  • CLS (Cumulative Layout Shift): quao pulante a pagina e'. Bom: < 0.1.

Thresholds oficiais (Google).

MetricaBom (p75)Needs improvementPoor
LCP≤ 2.5s≤ 4.0s> 4.0s
INP≤ 200ms≤ 500ms> 500ms
CLS≤ 0.1≤ 0.25> 0.25

p75 = 75% dos users estao abaixo desse valor. Use p75 (ou p90 em apps menores) como threshold.

web-vitals lib - medir direto no browser.

pnpm add web-vitals
import { onLCP, onINP, onCLS, onFCP, onTTFB } from "web-vitals";

onLCP((metric) => {
  // Envia pra backend (ou Sentry, Datadog, etc)
  sendMetric({
    name: metric.name,         // "LCP"
    value: metric.value,       // 1.234 (segundos)
    rating: metric.rating,     // "good" | "needs-improvement" | "poor"
    id: metric.id,             // ID unico
    page: location.pathname,   // contexto
  });
});

onINP((metric) => {
  sendMetric({ name: metric.name, value: metric.value, rating: metric.rating });
});

onCLS((metric) => {
  sendMetric({ name: metric.name, value: metric.value, rating: metric.rating });
});

Sentry Performance - RUM integrado.

Sentry.init({
  tracesSampleRate: 0.1,  // 10% dos requests viram performance
  integrations: [Sentry.browserTracingIntegration()],
});
// Sentry mede LCP, INP, CLS automaticamente
// + Web Vitals dashboards

Datadog RUM.

import { datadogRum } from "@datadog/browser-rum";

datadogRum.init({
  applicationId: "...",
  clientToken: "...",
  site: "datadoghq.com",
  // ...
});

New Relic Browser.

import { BrowserAgent } from "@newrelic/browser-agent";

const agent = new BrowserAgent({...});

Custom metrics - alem de Web Vitals. Meça o que importa pro seu app:

// Tempo de checkout
const start = performance.now();
await submitCheckout();
const duration = performance.now() - start;

Sentry.metrics.distribution("checkout.duration", duration, {
  tags: { country: "BR", plan: "pro" },
});

// Counter: quantos checkouts completaram
Sentry.metrics.increment("checkout.completed", 1, {
  tags: { country: "BR" },
});

PerformanceObserver - low-level access. Pra casos onde web-vitals nao mede:

const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    // Long tasks (>50ms) = possivel jank
    if (entry.entryType === "longtask") {
      console.log("Long task:", entry.duration, "ms");
      sendMetric({ name: "longtask", value: entry.duration });
    }
    // Resource timings (fetch, image, etc)
    if (entry.entryType === "resource") {
      const duration = entry.responseEnd - entry.startTime;
      sendMetric({ name: "resource.load", value: duration, url: entry.name });
    }
  }
});

observer.observe({ entryTypes: ["longtask", "resource", "navigation"] });

navigation timing - TTFB, DOM Content Loaded, Load.

const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntriesByType("navigation")) {
    sendMetric({ name: "ttfb", value: entry.responseStart });
    sendMetric({ name: "domContentLoaded", value: entry.domContentLoadedEventEnd });
    sendMetric({ name: "load", value: entry.loadEventEnd });
  }
});
observer.observe({ type: "navigation", buffered: true });

Session replay - reproduz o que o user viu. Sentry, LogRocket, Datadog gravam DOM + eventos do user. Reproduz como um video, com network tab visivel.

Sentry.init({
  replaysSessionSampleRate: 0.1,    // 10% de sessoes normais
  replaysOnErrorSampleRate: 1.0,    // 100% com erro
  replays: {
    maskAllText: true,    // privacy: textos viram ***
    blockAllMedia: true,  // privacy: imagens/video nao gravados
  },
});

Session replay e LGPD/GDPR. Gravar tudo (incluindo PII) sem consentimento = violacao. Sempre:

  1. maskAllText: true (textos viram ***).
  2. blockAllMedia: true (imagens nao gravadas).
  3. Consent mode - so' ativa replay se user aceitou cookies.

Threshold tuning. Web Vitals thresholds do Google sao base - ajuste por seu app:

  • E-commerce: LCP < 2.5s (impacto conversion).
  • Dashboard interno: LCP < 1.5s (user interno, espera menos).
  • Mobile 3G: LCP < 4.0s (rede lenta, expectativa diferente).

"RUM" vs "Synthetic monitoring".

  • Synthetic (Lighthouse, WebPageTest): roda em condicoes controladas (Chrome fixo, rede 4G simulada, sem interacao). Da baseline, mas nao reflete user real.
  • RUM (Sentry, Datadog): roda em users reais (Chrome/Safari/Firefox, Wi-Fi/4G/3G, com interacao). Da realidade.

Use os 2 juntos. Synthetic em CI (regressao automatica), RUM em prod (performance real).

Aprofundamento 🟡

p75 vs p50 vs p90. p75 e' o padrao Google (75% dos users). p50 (mediana) e' muito otimista (ignora cauda longa). p90 e' mais estrito (10% dos users podem ter performance ruim). Recomendado: p75 pra dashboards, p90 pra alertas.

"Rage clicks" e "dead clicks". Rage click = user clica 5+ vezes em 3 segundos (frustracao, achando que nao funcionou). Dead click = clique que nao dispara nenhuma acao (quebrou). Ferramentas como LogRocket e Sentry Session Replay detectam isso automaticamente.

Custom metrics - o que medir. Alem de Web Vitals:

  • Business metrics: checkout conversion, signup completion, search results.
  • App-specific: time to interactive (TTI), time to first byte (TTFB), long task count.
  • User behavior: feature usage, error recovery rate.

Real User Monitoring vs Real User Measurements (RUM). RUM = medir performance de users reais. RUM e' o termo padrao. "Real User Measurements" e' confuso (mesma sigla, significado diferente as vezes).

Attribution - "o que causou LCP ruim?". web-vitals da o element que causou LCP ruim:

onLCP((metric) => {
  // metric.attribution: detalhes de qual elemento
  // { element: HTMLElement, url, ... }
  console.log("LCP element:", metric.attribution.element);
  console.log("LCP url:", metric.attribution.url);
});

Use pra debugar - LCP ruim foi causado por imagem grande? Font load? HTML render-blocking?

INP debugging - "qual interacao demorou?". onINP da o evento que causou a interacao lenta:

onINP((metric) => {
  // metric.attribution: event handler, target, type
  console.log("Slow interaction:", metric.attribution.eventTarget);
  console.log("Type:", metric.attribution.eventType);
  console.log("Handler time:", metric.attribution.handlerTime);
});

CLS debugging - "o que pulou?". onCLS da os layout shifts que causaram o CLS:

onCLS((metric) => {
  // metric.attribution.largestShiftTarget
  // metric.attribution.largestShiftValue
  // metric.attribution.largestShiftTime
  console.log("Layout shift target:", metric.attribution.largestShiftTarget);
});

Cumulative Layout Shift ate o user clicar. CLS e' cumulativo - soma todos os shifts ate o user interagir. Use onCLS com reportAllChanges: true se quiser capturar cada shift:

onCLS((metric) => {
  // envia a cada shift significativo
  sendMetric({ name: "cls", value: metric.value });
}, { reportAllChanges: true });

Send rate - throttling. Em apps com muitos users, throttle envio de metricas pra nao sobrecarregar backend:

import { onLCP, onINP, onCLS } from "web-vitals";

// Throttle: agrupa eventos e envia a cada 30s
let buffer = [];
function flush() {
  if (buffer.length === 0) return;
  navigator.sendBeacon("/api/metrics", JSON.stringify(buffer));
  buffer = [];
}

[onLCP, onINP, onCLS].forEach((fn) =>
  fn((metric) => {
    buffer.push({ name: metric.name, value: metric.value });
  })
);

setInterval(flush, 30_000);

navigator.sendBeacon - envio assincrono nao-bloqueante. Ideal pra metricas (sobrevive a unload, nao bloqueia UI).

Pra quem quer ir mais alem 🔴

Lighthouse CI - synthetic em CI. Lighthouse roda em Chrome headless e mede Web Vitals + a11y + best practices. Lighthouse CI integra com GitHub Actions, falha PR se regressao. Complementa RUM: RUM = producao real, Lighthouse = baseline em CI.

WebPageTest - waterfail visual. WebPageTest roda em browsers reais (Chrome, Firefox, Safari) em locais (datacenters) e gera waterfall chart de cada request. Use pra debug de LCP ruim - ve qual recurso causou o delay.

Chrome User Experience Report (CrUX). Google coleta Web Vitals de Chrome users reais (com opt-in) e expoe como dataset publico. Field data oficial - "seu site vs competitors". Use pra benchmark.

Web Vitals attribution via Performance Timeline API. Low-level

  • PerformanceObserver com element e size entries da espec completa. Cobre em performance-web.

Cumulative Layout Shift em SPAs (React, Vue, Svelte). SPA tem muitas navegacoes sem page reload. CLS de cada navegacao se acumula? Nao - CLS e' reset por user interaction. SPA que tem pouca interacao (ex: landing page) pode acumular alto CLS. Cuidado com font swap (FOUT) - causa shifts grandes.

INP vs FID (First Input Delay). FID foi substituido por INP em 2024. FID media so' a primeira interacao - INP mede pior interacao na sessao. Use INP. FID deprecated em 2024, removido de CWV em 2024.

Leitura recomendada:

Dica: o erro mais comum em RUM e' medir e nao agir. Time instala Sentry, ve dashboards, e nada acontece com os dados. RUM sem alerting e' dashboard morto. Configure: (1) alerta quando p75 LCP > 4s, (2) budget de performance em CI (Lighthouse CI falha PR se regressao), (3) review mensal de Core Web Vitals. Medir e agir, nao "medir e mostrar em reuniao".

No proximo no, vamos OpenTelemetry Web: como emitir traces distribuidos do client pro server, com W3C Trace Context propagado via headers HTTP, e integracao com backend (Jaeger, Tempo, Honeycomb, Sentry, Datadog).

// Quiz

Qual a diferenca entre Synthetic monitoring (Lighthouse) e Real User Monitoring (RUM) e por que usar os dois?

Escolha uma alternativa

// recursos

// avaliação da trilha

—
ainda sem avaliações