Instrumentação Frontend com Grafana Faro Web SDK

O Collector FE é implantado na infraestrutura do cliente e recebe os dados do Faro SDK via HTTP, encaminhando para o Grafana Loki. Consulte a documentação do Collector FE para detalhes de deploy.


Índice

  • O que o Faro captura
  • Instalação
  • Configuração base
  • React
  • React Router v7 (Data Router)
  • React Router v6 (Data Router)
  • ErrorBoundary
  • Component Profiling
  • Identificação do usuário logado
  • Next.js
  • App Router (Next.js 14+)
  • Pages Router
  • Angular
  • Vue.js
  • Vanilla JavaScript / HTML
  • OpenTelemetry Tracing
  • Sinais customizados
  • Eventos
  • Logs
  • Medições
  • User Actions
  • Session Tracking
  • Web Vitals
  • Identificação de usuário
  • View Tracking
  • Sampling
  • CORS e segurança
  • Variáveis de ambiente por framework
  • Boas práticas
  • Troubleshooting
  • FAQ

O que o Faro captura

O Faro Web SDK v2 coleta automaticamente:

Sinal Descrição Exemplo
Web Vitals LCP, INP, CLS, FCP, TTFB (Web Vitals v5) LCP = 2.1s
Erros JavaScript Exceções não tratadas, com stacktrace TypeError: Cannot read property 'x' of undefined
Console Interceptação de console.log/warn/error/info console.error('Falha no checkout')
Navegação Mudanças de rota (SPA) e page loads /home/checkout
Sessões Tracking de sessão com lifecycle events session_start, session_resume
Eventos customizados Interações de negócio Clique em "Comprar", busca realizada
Logs customizados Logs estruturados enviados manualmente Debug info, warnings
Medições Métricas de performance da aplicação Tempo de renderização, tempo de busca
User Actions Interações do usuário com spans automáticos Click, submit, drag
Traces Distributed tracing via OpenTelemetry-JS Fetch requests com trace context

Instalação

NPM / Yarn / pnpm

# SDK base (obrigatório)
npm install @grafana/faro-web-sdk

# Tracing com OpenTelemetry (recomendado)
npm install @grafana/faro-web-tracing

# React (se aplicável)
npm install @grafana/faro-react

O pacote @grafana/faro-react já inclui tudo de @grafana/faro-web-sdk, então para projetos React basta instalar @grafana/faro-react + @grafana/faro-web-tracing.

CDN (sem bundler)

<script src="https://unpkg.com/@grafana/faro-web-sdk@^2/dist/bundle/faro-web-sdk.iife.js"></script>
<script>
  window.GrafanaFaroWebSdk.initializeFaro({
    url: 'https://collector-fe.meusite.com.br/collect/meu-tenant/eyJhbGci...',
    app: { name: 'minha-app', version: '1.0.0', environment: 'production' },
  });
</script>

Configuração base

Crie um arquivo dedicado para o Faro e importe-o antes de qualquer outro código no entry point da aplicação.

src/faro.ts

import { initializeFaro, getWebInstrumentations } from '@grafana/faro-web-sdk';
import { TracingInstrumentation } from '@grafana/faro-web-tracing';

export const faro = initializeFaro({
  url: import.meta.env.VITE_FARO_URL,

  app: {
    name: 'minha-app',
    version: import.meta.env.VITE_APP_VERSION || '0.0.0',
    environment: import.meta.env.MODE,  // 'development' | 'production'
  },

  sessionTracking: {
    enabled: true,
    persistent: true,         // mantém sessão entre reloads (localStorage)
    samplingRate: 1,           // 1 = 100% das sessões
  },

  batching: {
    enabled: true,
    sendTimeout: 250,          // ms entre batches
    itemLimit: 50,             // itens por batch
  },

  dedupe: {
    enabled: true,
    interval: 1000,            // ignora sinais duplicados em 1s
  },

  instrumentations: [
    ...getWebInstrumentations({
      captureConsole: true,
      captureConsoleDisabledLevels: [],  // captura todos os níveis
    }),

    new TracingInstrumentation({
      instrumentationOptions: {
        propagateTraceHeaderCorsUrls: [
          new RegExp(`${import.meta.env.VITE_API_URL}/*`),
        ],
      },
    }),
  ],
});

URL do Collector FE

A URL segue o formato:

https://<host-collector>/collect/<tenant>/<jwt-token>

O <tenant> identifica o produto/cliente no Loki, e o <jwt-token> é o JWT de autenticação. Consulte a documentação do Collector FE para gerar o token.


React

React Router v7 (Data Router)

// src/faro.ts
import {
  initializeFaro,
  getWebInstrumentations,
  ReactIntegration,
  createReactRouterV7DataOptions,
} from '@grafana/faro-react';
import { TracingInstrumentation } from '@grafana/faro-web-tracing';
import { matchRoutes } from 'react-router';

export const faro = initializeFaro({
  url: import.meta.env.VITE_FARO_URL,
  app: {
    name: 'minha-app-react',
    version: import.meta.env.VITE_APP_VERSION || '0.0.0',
    environment: import.meta.env.MODE,
  },
  sessionTracking: {
    enabled: true,
    persistent: true,
    samplingRate: 1,
  },
  instrumentations: [
    ...getWebInstrumentations({ captureConsole: true }),
    new TracingInstrumentation({
      instrumentationOptions: {
        propagateTraceHeaderCorsUrls: [/https:\/\/api\.meusite\.com\.br\/.*/],
      },
    }),
    new ReactIntegration({
      router: createReactRouterV7DataOptions({ matchRoutes }),
    }),
  ],
});
// src/router.ts
import { createBrowserRouter } from 'react-router';
import { withFaroRouterInstrumentation } from '@grafana/faro-react';

const baseRouter = createBrowserRouter([
  {
    path: '/',
    element: <Layout />,
    children: [
      { index: true, element: <Home /> },
      { path: 'products/:id', element: <ProductDetail /> },
      { path: 'checkout', element: <Checkout /> },
    ],
  },
]);

export const router = withFaroRouterInstrumentation(baseRouter);
// src/main.tsx
import './faro';  // Importar ANTES de tudo
import { RouterProvider } from 'react-router';
import { router } from './router';

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <RouterProvider router={router} />
  </StrictMode>
);

React Router v6 (Data Router)

// src/faro.ts
import {
  initializeFaro,
  getWebInstrumentations,
  ReactIntegration,
  createReactRouterV6DataOptions,
} from '@grafana/faro-react';
import { TracingInstrumentation } from '@grafana/faro-web-tracing';
import { matchRoutes } from 'react-router-dom';

export const faro = initializeFaro({
  url: import.meta.env.VITE_FARO_URL,
  app: {
    name: 'minha-app-react',
    version: import.meta.env.VITE_APP_VERSION || '0.0.0',
    environment: import.meta.env.MODE,
  },
  instrumentations: [
    ...getWebInstrumentations({ captureConsole: true }),
    new TracingInstrumentation(),
    new ReactIntegration({
      router: createReactRouterV6DataOptions({ matchRoutes }),
    }),
  ],
});
// src/router.ts
import { createBrowserRouter } from 'react-router-dom';
import { withFaroRouterInstrumentation } from '@grafana/faro-react';

const baseRouter = createBrowserRouter([/* routes */]);
export const router = withFaroRouterInstrumentation(baseRouter);

ErrorBoundary

O @grafana/faro-react inclui um ErrorBoundary que captura erros de renderização e envia automaticamente para o Faro:

import { FaroErrorBoundary } from '@grafana/faro-react';

function App() {
  return (
    <FaroErrorBoundary
      fallback={(error, resetError) => (
        <div>
          <h2>Algo deu errado</h2>
          <p>{error.message}</p>
          <button onClick={resetError}>Tentar novamente</button>
        </div>
      )}
    >
      <RouterProvider router={router} />
    </FaroErrorBoundary>
  );
}

Pode ser usado em qualquer nível da árvore de componentes para capturar erros granularmente:

<FaroErrorBoundary fallback={<ErrorFallback />}>
  <CheckoutForm />
</FaroErrorBoundary>

<FaroErrorBoundary fallback={<ErrorFallback />}>
  <ProductRecommendations />
</FaroErrorBoundary>

Component Profiling

Meça o tempo de renderização de componentes específicos:

import { withFaroProfiler } from '@grafana/faro-react';

function ProductList({ products }: Props) {
  return (
    <ul>
      {products.map((p) => (
        <li key={p.id}>{p.name}</li>
      ))}
    </ul>
  );
}

export default withFaroProfiler(ProductList);

Os dados de profiling são enviados como measurements com o tipo component_render e incluem tempos de mount e update.

Identificação do usuário logado

Sincronize o usuário autenticado com o Faro para correlacionar sessões com usuários reais:

// src/components/FaroUserSync.tsx
import { useEffect } from 'react';
import { faro } from '../faro';

interface User {
  id: string;
  name: string;
  email: string;
  role?: string;
}

export function FaroUserSync({ user }: { user: User | null }) {
  useEffect(() => {
    if (user) {
      faro.api.setUser({
        id: user.id,
        username: user.name,
        email: user.email,
        attributes: {
          role: user.role ?? 'user',
        },
      });
    } else {
      faro.api.resetUser();
    }
  }, [user]);

  return null;
}
// src/App.tsx
import { FaroUserSync } from './components/FaroUserSync';
import { useAuth } from './hooks/useAuth';

function App() {
  const { user } = useAuth();

  return (
    <>
      <FaroUserSync user={user} />
      <RouterProvider router={router} />
    </>
  );
}

Next.js

App Router (Next.js 14+)

Com o App Router, o Faro deve ser inicializado apenas no client side:

// src/lib/faro.ts
'use client';

import { initializeFaro, getWebInstrumentations } from '@grafana/faro-web-sdk';
import { TracingInstrumentation } from '@grafana/faro-web-tracing';

let faroInstance: ReturnType<typeof initializeFaro> | null = null;

export function getFaro() {
  if (typeof window === 'undefined') return null;
  if (faroInstance) return faroInstance;

  faroInstance = initializeFaro({
    url: process.env.NEXT_PUBLIC_FARO_URL!,
    app: {
      name: process.env.NEXT_PUBLIC_APP_NAME || 'minha-app-next',
      version: process.env.NEXT_PUBLIC_APP_VERSION || '0.0.0',
      environment: process.env.NODE_ENV,
    },
    sessionTracking: {
      enabled: true,
      persistent: true,
      samplingRate: 1,
    },
    instrumentations: [
      ...getWebInstrumentations({ captureConsole: true }),
      new TracingInstrumentation({
        instrumentationOptions: {
          propagateTraceHeaderCorsUrls: [
            new RegExp(`${process.env.NEXT_PUBLIC_API_URL}/*`),
          ],
        },
      }),
    ],
  });

  return faroInstance;
}
// src/components/FaroProvider.tsx
'use client';

import { useEffect } from 'react';
import { getFaro } from '@/lib/faro';

export function FaroProvider({ children }: { children: React.ReactNode }) {
  useEffect(() => {
    getFaro();
  }, []);

  return <>{children}</>;
}
// src/app/layout.tsx
import { FaroProvider } from '@/components/FaroProvider';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="pt-BR">
      <body>
        <FaroProvider>
          {children}
        </FaroProvider>
      </body>
    </html>
  );
}

Sincronizar usuário logado (Next.js + NextAuth/Auth.js):

// src/components/FaroUserSync.tsx
'use client';

import { useEffect } from 'react';
import { useSession } from 'next-auth/react';
import { getFaro } from '@/lib/faro';

export function FaroUserSync() {
  const { data: session } = useSession();

  useEffect(() => {
    const faro = getFaro();
    if (!faro) return;

    if (session?.user) {
      faro.api.setUser({
        id: session.user.id ?? '',
        username: session.user.name ?? '',
        email: session.user.email ?? '',
      });
    } else {
      faro.api.resetUser();
    }
  }, [session]);

  return null;
}

Pages Router

// pages/_app.tsx
import type { AppProps } from 'next/app';
import { useEffect } from 'react';
import { initializeFaro, getWebInstrumentations } from '@grafana/faro-web-sdk';
import { TracingInstrumentation } from '@grafana/faro-web-tracing';

let initialized = false;

export default function MyApp({ Component, pageProps }: AppProps) {
  useEffect(() => {
    if (initialized) return;
    initialized = true;

    initializeFaro({
      url: process.env.NEXT_PUBLIC_FARO_URL!,
      app: {
        name: 'minha-app-next',
        version: process.env.NEXT_PUBLIC_APP_VERSION || '0.0.0',
        environment: process.env.NODE_ENV,
      },
      sessionTracking: { enabled: true, persistent: true },
      instrumentations: [
        ...getWebInstrumentations({ captureConsole: true }),
        new TracingInstrumentation(),
      ],
    });
  }, []);

  return <Component {...pageProps} />;
}

Angular

O Faro não tem um pacote específico para Angular, mas integra-se usando o SDK base com um service:

npm install @grafana/faro-web-sdk @grafana/faro-web-tracing
// src/app/services/faro.service.ts
import { Injectable } from '@angular/core';
import { Faro, initializeFaro, getWebInstrumentations } from '@grafana/faro-web-sdk';
import { TracingInstrumentation } from '@grafana/faro-web-tracing';
import { Router, NavigationEnd } from '@angular/router';
import { filter } from 'rxjs/operators';
import { environment } from '../../environments/environment';

@Injectable({ providedIn: 'root' })
export class FaroService {
  private faro: Faro | null = null;

  constructor(private router: Router) {}

  initialize(): void {
    this.faro = initializeFaro({
      url: environment.faroUrl,
      app: {
        name: environment.appName,
        version: environment.appVersion,
        environment: environment.production ? 'production' : 'development',
      },
      sessionTracking: {
        enabled: true,
        persistent: true,
        samplingRate: 1,
      },
      instrumentations: [
        ...getWebInstrumentations({ captureConsole: true }),
        new TracingInstrumentation({
          instrumentationOptions: {
            propagateTraceHeaderCorsUrls: [new RegExp(`${environment.apiUrl}/*`)],
          },
        }),
      ],
    });

    this.trackRouteChanges();
  }

  private trackRouteChanges(): void {
    this.router.events
      .pipe(filter((event): event is NavigationEnd => event instanceof NavigationEnd))
      .subscribe((event) => {
        this.faro?.api.pushEvent('navigation', {
          route: event.urlAfterRedirects,
        });
        this.faro?.api.setView({ name: event.urlAfterRedirects });
      });
  }

  setUser(user: { id: string; name: string; email: string }): void {
    this.faro?.api.setUser({
      id: user.id,
      username: user.name,
      email: user.email,
    });
  }

  resetUser(): void {
    this.faro?.api.resetUser();
  }

  pushEvent(name: string, attributes?: Record<string, string>): void {
    this.faro?.api.pushEvent(name, attributes);
  }

  pushLog(message: string, level: 'info' | 'warn' | 'error' = 'info'): void {
    this.faro?.api.pushLog([message], { level: level as any });
  }

  get instance(): Faro | null {
    return this.faro;
  }
}
// src/app/app.component.ts
import { Component, OnInit } from '@angular/core';
import { FaroService } from './services/faro.service';

@Component({
  selector: 'app-root',
  template: '<router-outlet />',
})
export class AppComponent implements OnInit {
  constructor(private faroService: FaroService) {}

  ngOnInit(): void {
    this.faroService.initialize();
  }
}

ErrorHandler global para capturar erros Angular:

// src/app/services/faro-error-handler.ts
import { ErrorHandler, Injectable } from '@angular/core';
import { FaroService } from './faro.service';

@Injectable()
export class FaroErrorHandler implements ErrorHandler {
  constructor(private faroService: FaroService) {}

  handleError(error: Error): void {
    const faro = this.faroService.instance;
    if (faro) {
      faro.api.pushError(error);
    }
    console.error(error);
  }
}
// src/app/app.module.ts
import { ErrorHandler, NgModule } from '@angular/core';
import { FaroErrorHandler } from './services/faro-error-handler';

@NgModule({
  providers: [
    { provide: ErrorHandler, useClass: FaroErrorHandler },
  ],
})
export class AppModule {}

Vue.js

npm install @grafana/faro-web-sdk @grafana/faro-web-tracing
// src/plugins/faro.ts
import { initializeFaro, getWebInstrumentations, type Faro } from '@grafana/faro-web-sdk';
import { TracingInstrumentation } from '@grafana/faro-web-tracing';
import type { App, Plugin } from 'vue';
import type { Router } from 'vue-router';

let faroInstance: Faro | null = null;

export const faroPlugin: Plugin = {
  install(app: App, options: { url: string; appName: string; version: string; router: Router }) {
    faroInstance = initializeFaro({
      url: options.url,
      app: {
        name: options.appName,
        version: options.version,
        environment: import.meta.env.MODE,
      },
      sessionTracking: { enabled: true, persistent: true },
      instrumentations: [
        ...getWebInstrumentations({ captureConsole: true }),
        new TracingInstrumentation(),
      ],
    });

    options.router.afterEach((to) => {
      faroInstance?.api.setView({ name: to.path });
      faroInstance?.api.pushEvent('navigation', { route: to.path });
    });

    app.config.errorHandler = (err, instance, info) => {
      if (err instanceof Error) {
        faroInstance?.api.pushError(err);
      }
      console.error(err);
    };

    app.provide('faro', faroInstance);
  },
};

export function useFaro(): Faro | null {
  return faroInstance;
}
// src/main.ts
import { createApp } from 'vue';
import { createRouter, createWebHistory } from 'vue-router';
import App from './App.vue';
import { faroPlugin } from './plugins/faro';

const router = createRouter({
  history: createWebHistory(),
  routes: [/* ... */],
});

const app = createApp(App);

app.use(faroPlugin, {
  url: import.meta.env.VITE_FARO_URL,
  appName: 'minha-app-vue',
  version: import.meta.env.VITE_APP_VERSION || '0.0.0',
  router,
});

app.use(router);
app.mount('#app');

Vanilla JavaScript / HTML

<!DOCTYPE html>
<html lang="pt-BR">
<head>
  <meta charset="UTF-8">
  <title>Minha App</title>
  <script src="https://unpkg.com/@grafana/faro-web-sdk@^2/dist/bundle/faro-web-sdk.iife.js"></script>
  <script>
    var faro = window.GrafanaFaroWebSdk.initializeFaro({
      url: 'https://collector-fe.meusite.com.br/collect/meu-tenant/eyJhbGci...',
      app: {
        name: 'minha-app',
        version: '1.0.0',
        environment: 'production',
      },
      sessionTracking: { enabled: true, persistent: true },
    });

    // Capturar erros globais
    window.addEventListener('error', function(event) {
      faro.api.pushError(new Error(event.message));
    });

    // Identificar usuário (chamar após login)
    function setFaroUser(id, name, email) {
      faro.api.setUser({ id: id, username: name, email: email });
    }
  </script>
</head>
<body>
  <!-- Conteúdo da aplicação -->
</body>
</html>

OpenTelemetry Tracing

O tracing conecta o frontend ao backend, permitindo rastrear uma requisição do clique do usuário até o banco de dados.

Configuração com TracingInstrumentation

import { TracingInstrumentation } from '@grafana/faro-web-tracing';

new TracingInstrumentation({
  instrumentationOptions: {
    // URLs para as quais enviar trace headers (W3C Trace Context)
    propagateTraceHeaderCorsUrls: [
      new RegExp('https://api\\.meusite\\.com\\.br/.*'),
      'https://checkout-api.meusite.com.br',
    ],
    // Opções do fetch instrumentation
    fetchInstrumentationOptions: {
      ignoreNetworkEvents: true,  // reduz ruído de eventos de rede
    },
  },
})

Criar spans manuais

const { trace, context } = faro.api.getOTEL();

const tracer = trace.getTracer('minha-app');

// Span simples
const span = tracer.startSpan('checkout.process');
try {
  await processCheckout(cart);
  span.setStatus({ code: 1 }); // OK
} catch (error) {
  span.setStatus({ code: 2, message: error.message }); // ERROR
  span.recordException(error);
  throw error;
} finally {
  span.end();
}

Span com contexto propagado

const { trace, context } = faro.api.getOTEL();
const tracer = trace.getTracer('minha-app');

const parentSpan = tracer.startSpan('user.action');

context.with(trace.setSpan(context.active(), parentSpan), async () => {
  // Todas as chamadas fetch dentro deste contexto herdam o trace
  const response = await fetch('https://api.meusite.com.br/orders', {
    method: 'POST',
    body: JSON.stringify(orderData),
  });

  const childSpan = tracer.startSpan('parse.response');
  const data = await response.json();
  childSpan.end();

  parentSpan.end();
});

Sinais customizados

Eventos

Eventos rastreiam interações de negócio e comportamento do usuário:

// Evento de conversão
faro.api.pushEvent('purchase_completed', {
  orderId: 'ORD-12345',
  total: '299.90',
  items: '3',
  paymentMethod: 'credit_card',
}, 'checkout');

// Evento de busca
faro.api.pushEvent('search_performed', {
  query: 'camiseta azul',
  resultsCount: '42',
  page: '/busca',
}, 'search');

// Evento de feature toggle
faro.api.pushEvent('feature_flag_evaluated', {
  flag: 'new-checkout-flow',
  variant: 'treatment',
  userId: 'usr-123',
}, 'experimentation');

// Evento de erro de formulário
faro.api.pushEvent('form_validation_failed', {
  form: 'checkout',
  fields: 'cpf,cep',
  attempt: '2',
}, 'forms');

Logs

Logs estruturados com nível e contexto:

import { LogLevel } from '@grafana/faro-web-sdk';

// Info
faro.api.pushLog(['Checkout iniciado para o pedido ORD-12345'], {
  level: LogLevel.INFO,
  context: {
    cartItems: '3',
    totalValue: '299.90',
  },
});

// Warning
faro.api.pushLog(['Estoque baixo para produto SKU-789'], {
  level: LogLevel.WARN,
  context: {
    sku: 'SKU-789',
    remaining: '2',
  },
});

// Error
faro.api.pushLog(['Falha ao processar pagamento'], {
  level: LogLevel.ERROR,
  context: {
    gateway: 'stripe',
    errorCode: 'card_declined',
    orderId: 'ORD-12345',
  },
});

// Debug
faro.api.pushLog(['WebSocket reconectado após 3 tentativas'], {
  level: LogLevel.DEBUG,
  context: {
    attempts: '3',
    latencyMs: '1250',
  },
});

Medições

Métricas de performance específicas da aplicação:

// Tempo de renderização de um componente
faro.api.pushMeasurement({
  type: 'component_performance',
  values: {
    product_list_render_ms: 142.3,
    product_count: 48,
  },
});

// Tempo de resposta da API percebido pelo usuário
faro.api.pushMeasurement({
  type: 'api_latency',
  values: {
    search_request_ms: 273,
    results_render_ms: 45,
    total_perceived_ms: 318,
  },
});

// Métricas de cache
faro.api.pushMeasurement({
  type: 'cache_stats',
  values: {
    hit_rate: 0.87,
    size_mb: 12.4,
    evictions: 3,
  },
});

User Actions

O Faro v2 rastreia interações do usuário automaticamente, criando spans que agrupam todos os efeitos colaterais (fetch requests, DOM updates, etc.) de uma ação.

Automático via atributo HTML

<button data-faro-user-action-name="add-to-cart">
  Adicionar ao carrinho
</button>

<form data-faro-user-action-name="checkout-submit">
  <!-- campos -->
  <button type="submit">Finalizar compra</button>
</form>

Programático

// Iniciar uma ação manual com controle total
const action = faro.api.startUserAction('complete-checkout', {
  importance: 'critical',  // 'normal' | 'critical'
});

try {
  await submitOrder(cart);
  action?.end();  // Finalizar com sucesso
} catch (error) {
  action?.end();
  faro.api.pushError(error);
}

User actions completam automaticamente 100ms após o último evento vinculado. Para requests pendentes, o timeout é configurável até 10 segundos.


Session Tracking

Sessões agrupam todas as interações de um usuário em uma visita.

Comportamento padrão

  • Duração máxima: 4 horas
  • Timeout de inatividade: 15 minutos
  • Persistência: com persistent: true, a sessão sobrevive a reloads da página (via localStorage)
  • Eventos de lifecycle: session_start, session_resume, session_extend

Configuração

initializeFaro({
  // ...
  sessionTracking: {
    enabled: true,
    persistent: true,        // sobrevive a page reloads
    samplingRate: 1,          // 1 = todas as sessões, 0.5 = 50%
  },
});

Acessar o ID da sessão

const sessionId = faro.api.getSession()?.id;

O header x-faro-session-id é automaticamente incluído nas requests para o Collector FE, permitindo correlação com dados de backend.


Web Vitals

O Faro v2 captura automaticamente as Web Vitals v5:

Métrica Nome Bom Precisa melhorar Ruim
LCP Largest Contentful Paint ≤ 2.5s ≤ 4.0s > 4.0s
INP Interaction to Next Paint ≤ 200ms ≤ 500ms > 500ms
CLS Cumulative Layout Shift ≤ 0.1 ≤ 0.25 > 0.25
FCP First Contentful Paint ≤ 1.8s ≤ 3.0s > 3.0s
TTFB Time to First Byte ≤ 800ms ≤ 1800ms > 1800ms

Faro v2: A métrica FID (First Input Delay) foi removida e substituída por INP (Interaction to Next Paint). A atribuição de Web Vitals é coletada por padrão.

Não é necessário nenhuma configuração adicional — as Web Vitals são capturadas automaticamente pelo getWebInstrumentations().


Identificação de usuário

Permite correlacionar sessões com usuários reais no Grafana:

// Após login
faro.api.setUser({
  id: 'usr-12345',
  username: 'João Silva',
  email: 'joao@empresa.com.br',
  attributes: {
    role: 'admin',
    plan: 'enterprise',
    company: 'Acme Corp',
  },
});

// Após logout
faro.api.resetUser();

Nunca envie dados sensíveis (CPF, senha, tokens) nos atributos do usuário.


View Tracking

Marque mudanças de "view" manualmente para SPAs que não usam router integrado:

// Ao navegar para uma nova seção
faro.api.setView({ name: '/dashboard' });

// Com atributos
faro.api.setView({
  name: '/product/:id',
  attributes: {
    productId: '123',
    category: 'electronics',
  },
});

Para frameworks com integração de router (React Router, Next.js), o view tracking é automático.


Sampling

Controle o volume de dados enviados:

initializeFaro({
  // ...
  sessionTracking: {
    samplingRate: 0.1,  // Captura 10% das sessões
  },
});
Cenário samplingRate recomendado
Desenvolvimento 1 (100%)
Staging 1 (100%)
Produção — baixo tráfego (< 10k DAU) 1 (100%)
Produção — médio tráfego (10k-100k DAU) 0.5 (50%)
Produção — alto tráfego (> 100k DAU) 0.1 - 0.25

O sampling é por sessão: quando uma sessão é amostrada, todos os eventos daquela sessão são capturados. Isso garante que você tem a visão completa de cada sessão amostrada.


CORS e segurança

ALLOW_ORIGINS no Collector FE

O Collector FE valida a origem das requests via CORS. Configure ALLOW_ORIGINS com os domínios da sua aplicação:

ALLOW_ORIGINS=https://app.meusite.com.br,https://*.meusite.com.br

Content Security Policy (CSP)

Se sua aplicação usa CSP, adicione o domínio do Collector FE:

Content-Security-Policy: connect-src 'self' https://collector-fe.meusite.com.br;

Dados sensíveis

O Faro captura URLs, console logs e payloads de erro. Garanta que:

  • Tokens e credenciais nunca apareçam em URLs
  • Console logs não contenham dados PII
  • Mensagens de erro não exponham dados de negócio

Variáveis de ambiente por framework

React (Vite)

# .env
VITE_FARO_URL=https://collector-fe.meusite.com.br/collect/meu-tenant/eyJhbGci...
VITE_APP_VERSION=1.2.3
VITE_API_URL=https://api.meusite.com.br

React (Create React App)

# .env
REACT_APP_FARO_URL=https://collector-fe.meusite.com.br/collect/meu-tenant/eyJhbGci...
REACT_APP_VERSION=1.2.3

Next.js

# .env.local
NEXT_PUBLIC_FARO_URL=https://collector-fe.meusite.com.br/collect/meu-tenant/eyJhbGci...
NEXT_PUBLIC_APP_VERSION=1.2.3
NEXT_PUBLIC_APP_NAME=minha-app-next
NEXT_PUBLIC_API_URL=https://api.meusite.com.br

Angular

// src/environments/environment.prod.ts
export const environment = {
  production: true,
  faroUrl: 'https://collector-fe.meusite.com.br/collect/meu-tenant/eyJhbGci...',
  appName: 'minha-app-angular',
  appVersion: '1.2.3',
  apiUrl: 'https://api.meusite.com.br',
};

Vue.js (Vite)

# .env
VITE_FARO_URL=https://collector-fe.meusite.com.br/collect/meu-tenant/eyJhbGci...
VITE_APP_VERSION=1.2.3

Boas práticas

Inicialização

  1. Inicialize o Faro antes de qualquer outro código — isso garante que erros durante a inicialização da app sejam capturados
  2. Separe a configuração em um arquivo dedicado (src/faro.ts) e importe no entry point
  3. Use variáveis de ambiente para a URL do Collector FE — nunca hardcode tokens no código

Eventos e logs

  1. Nomeie eventos com padrão consistente — use snake_case e prefixos por domínio: checkout.completed, search.performed, auth.login_failed
  2. Não envie dados sensíveis — CPF, senhas, tokens, números de cartão nunca devem aparecer em eventos, logs ou atributos
  3. Use domínios para agrupar eventos — o terceiro parâmetro de pushEvent agrupa eventos logicamente

Performance

  1. Habilite batching — reduz o número de requests HTTP para o Collector FE
  2. Habilite dedupe — evita envio de sinais duplicados (erros em loop, por exemplo)
  3. Ajuste o samplingRate em produção — para apps com alto tráfego, 10-25% é suficiente

Tracing

  1. Configure propagateTraceHeaderCorsUrls — sem isso, as requests para APIs externas não terão trace context
  2. Inclua apenas APIs próprias — nunca propague trace headers para APIs de terceiros (pode vazar informação)

Deploy

  1. Faça upload de source maps — sem eles, os stacktraces de erros ficam ilegíveis em produção
  2. Versionamento — sempre preencha app.version com a versão real do deploy para correlacionar erros com releases

Troubleshooting

Nenhum dado aparece no Grafana

  1. Abra o DevTools do browser → aba Network
  2. Filtre por collect para encontrar as requests do Faro
  3. Verifique: * Status 200: dados estão sendo enviados com sucesso * Status 0 / CORS error: o domínio não está em ALLOW_ORIGINS do Collector FE * Status 401: o JWT na URL é inválido * Nenhuma request: o Faro não foi inicializado (verifique o import no entry point)

Erros CORS no console

Access to fetch at 'https://collector-fe...' has been blocked by CORS policy

Solução: Adicione o domínio da sua aplicação em ALLOW_ORIGINS no Collector FE e reinicie o serviço.

Traces não propagam para o backend

  1. Verifique que TracingInstrumentation está nas instrumentations
  2. Confirme que a URL da API está em propagateTraceHeaderCorsUrls
  3. O backend deve aceitar os headers traceparent e tracestate via CORS

Web Vitals não aparecem

  • Web Vitals são coletadas apenas em page loads reais (não em SPAs sem reload)
  • Algumas métricas (LCP, CLS) só são reportadas quando o usuário sai da página ou ela vai para background
  • Em desenvolvimento com Hot Module Replacement, os valores podem ser inconsistentes

Console logs não são capturados

Verifique que captureConsole: true está configurado:

...getWebInstrumentations({
  captureConsole: true,
  captureConsoleDisabledLevels: [],  // sem essa linha, 'debug' e 'trace' são ignorados
}),

Dados duplicados

Habilite o dedupe:

initializeFaro({
  dedupe: { enabled: true, interval: 1000 },
  // ...
});

FAQ

O Faro SDK aumenta o bundle size da minha aplicação?

O @grafana/faro-web-sdk tem \~15 KB gzipped. O @grafana/faro-web-tracing adiciona mais \~45 KB gzipped (por incluir OpenTelemetry-JS). Use tree-shaking e lazy loading para minimizar o impacto.

Posso usar o Faro com micro-frontends / composable frontends?

Sim. Inicialize o Faro uma vez no shell/host application e compartilhe a instância com os micro-frontends. Use faro.api.setView() para marcar qual micro-frontend está ativo.

O Faro funciona com SSR (Server-Side Rendering)?

O Faro é client-side only. Em frameworks com SSR (Next.js, Nuxt), garanta que a inicialização ocorra apenas no browser (use typeof window !== 'undefined' ou diretivas 'use client').

Posso usar o Faro sem o TracingInstrumentation?

Sim. O tracing é opcional. Sem ele, você ainda captura Web Vitals, erros, console logs, eventos, medições e sessões.

O que acontece se o Collector FE estiver fora do ar?

O Faro descarta os dados silenciosamente — não causa erros visíveis para o usuário nem afeta a performance da aplicação.

Como faço para filtrar dados no Grafana por aplicação?

Use o label app no Loki:

{app="minha-app-react"} | json

Para filtrar por tipo de dado:

{app="minha-app-react", kind="exception"} | json

Qual a diferença entre pushEvent e pushLog?

  • pushEvent é para interações de negócio (compras, buscas, cliques) — aparece como evento com nome e atributos
  • pushLog é para informações operacionais (debug, warnings, erros) — aparece como log com nível e contexto

Como funciona o sampling por sessão?

Quando samplingRate: 0.1, o Faro decide na criação da sessão se ela será amostrada (10% de chance). Se a sessão for amostrada, todos os dados daquela sessão são capturados. Sessões não amostradas não enviam nenhum dado.