Kurs Next.js · Moduł 8: Deployment i produkcja
Monitoring wydajności i analityka błędów
W tej lekcji12
W produkcyjnych aplikacjach Next.js, monitoring wydajności i śledzenie błędów są kluczowe dla utrzymania wysokiej jakości dostarczanych usług. W tym module poznamy narzędzia i techniki, które pozwolą skutecznie monitorować aplikację, wykrywać problemy przed użytkownikami i optymalizować wydajność na bieżąco.
Znaczenie monitoringu w aplikacjach produkcyjnych
Monitoring to proces ciągłego obserwowania aplikacji w celu:
- Wczesnego wykrywania problemów - zanim dotkną użytkowników
- Śledzenia wydajności - czasy ładowania, Core Web Vitals, przepustowość
- Analizy zachowań użytkowników - jak użytkownicy korzystają z aplikacji
- Identyfikacji waskóch gardeł - miejsca wymagające optymalizacji
- Zapewnienia SLA - spełniania uzgodnionych poziomów usług
Typy monitoringu
1. Monitoring wydajności (Performance Monitoring)
- Core Web Vitals (LCP, FID, CLS)
- Czasy ładowania stron
- Czasy odpowiedzi API
- Zużycie zasobów (CPU, pamięć)
2. Monitoring błędów (Error Monitoring)
- JavaScript errors
- API errors
- Network errors
- Stack traces
3. Monitoring infrastruktury
- Dostępność serwerów
- Zużycie zasobów
- Metryki sieciowe
4. User Experience Monitoring (UX)
- Real User Monitoring (RUM)
- Session recordings
- Heat maps
- User flows
Integracja z Sentry dla monitoring błędów
Sentry to jedno z najpopularniejszych narzędzi do monitoringu błędów w aplikacjach JavaScript i Next.js.
1. Instalacja i konfiguracja Sentry
1npm install @sentry/nextjs
2# lub
3yarn add @sentry/nextjsUtwórz pliki konfiguracyjne:
1// sentry.client.config.js
2import * as Sentry from '@sentry/nextjs';
3
4Sentry.init({
5 dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
6
7 // Ustawienia środowiska
8 environment: process.env.NODE_ENV,
9
10 // Sample rate dla transakcji wydajnościowych
11 tracesSampleRate: 1.0,
12
13 // Włącz Replay dla sesji użytkowników
14 replaysSessionSampleRate: 0.1, // 10% sesji
15 replaysOnErrorSampleRate: 1.0, // 100% sesji z błędami
16
17 // Integracje
18 integrations: [
19 new Sentry.Replay({
20 maskAllText: false,
21 blockAllMedia: false,
22 }),
23 ],
24
25 // Filtrowanie błędów
26 beforeSend(event) {
27 // Ignoruj pewne typy błędów
28 if (event.exception) {
29 const error = event.exception.values[0];
30 if (error.type === 'ChunkLoadError') {
31 return null; // Ignoruj błędy ładowania chunków
32 }
33 }
34 return event;
35 },
36});1// sentry.server.config.js
2import * as Sentry from '@sentry/nextjs';
3
4Sentry.init({
5 dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
6 environment: process.env.NODE_ENV,
7 tracesSampleRate: 1.0,
8
9 // Dodatkowe opcje dla serwera
10 debug: false,
11
12 // Integracje specyficzne dla Node.js
13 integrations: [
14 new Sentry.Integrations.Http({ tracing: true }),
15 ],
16});1// sentry.edge.config.js
2import * as Sentry from '@sentry/nextjs';
3
4Sentry.init({
5 dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
6 environment: process.env.NODE_ENV,
7 tracesSampleRate: 1.0,
8 debug: false,
9});2. Konfiguracja Next.js dla Sentry
1// next.config.js
2const { withSentryConfig } = require('@sentry/nextjs');
3
4const nextConfig = {
5 // Twoja standardowa konfiguracja Next.js
6};
7
8const sentryWebpackPluginOptions = {
9 // Dodatkowe opcje konfiguracyjne dla Sentry webpack plugin
10 silent: true, // Wycisz logi Sentry podczas budowania
11 org: process.env.SENTRY_ORG,
12 project: process.env.SENTRY_PROJECT,
13};
14
15module.exports = withSentryConfig(nextConfig, sentryWebpackPluginOptions);3. Dodawanie kontekstu do błędów
1// lib/sentry.ts
2import * as Sentry from '@sentry/nextjs';
3
4// Funkcja do dodawania kontekstu użytkownika
5export function setUserContext(user: { id: string; email: string; name?: string }) {
6 Sentry.setUser({
7 id: user.id,
8 email: user.email,
9 username: user.name,
10 });
11}
12
13// Funkcja do dodawania tagów
14export function addTags(tags: Record<string, string>) {
15 Sentry.setTags(tags);
16}
17
18// Funkcja do manualnego raportowania błędów
19export function captureException(error: Error, context?: Record<string, any>) {
20 Sentry.withScope((scope) => {
21 if (context) {
22 scope.setContext('additional_info', context);
23 }
24 Sentry.captureException(error);
25 });
26}
27
28// Funkcja do śledzenia wydajności
29export function startTransaction(name: string, op: string) {
30 return Sentry.startTransaction({ name, op });
31}4. Wykorzystanie Sentry w komponentach
1// components/ErrorBoundary.tsx
2'use client';
3
4import React from 'react';
5import * as Sentry from '@sentry/nextjs';
6
7interface ErrorBoundaryState {
8 hasError: boolean;
9 eventId?: string;
10}
11
12class ErrorBoundary extends React.Component<
13 React.PropsWithChildren<{}>,
14 ErrorBoundaryState
15> {
16 constructor(props: React.PropsWithChildren<{}>) {
17 super(props);
18 this.state = { hasError: false };
19 }
20
21 static getDerivedStateFromError(_: Error): ErrorBoundaryState {
22 return { hasError: true };
23 }
24
25 componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
26 const eventId = Sentry.captureException(error, {
27 contexts: {
28 react: {
29 componentStack: errorInfo.componentStack,
30 },
31 },
32 });
33
34 this.setState({ eventId });
35 }
36
37 render() {
38 if (this.state.hasError) {
39 return (
40 <div className="error-fallback">
41 <h2>Ups! Coś poszło nie tak</h2>
42 <p>Wystąpił nieoczekiwany błąd. Nasz zespół został powiadomiony.</p>
43 <details style={{ whiteSpace: 'pre-wrap' }}>
44 ID błędu: {this.state.eventId}
45 </details>
46 <button onClick={() => window.location.reload()}>
47 Odśwież stronę
48 </button>
49 </div>
50 );
51 }
52
53 return this.props.children;
54 }
55}
56
57export default ErrorBoundary;Monitoring Core Web Vitals
Core Web Vitals to kluczowe metryki wydajności, które Google używa do oceny doświadczenia użytkownika:
- LCP (Largest Contentful Paint) - czas ładowania największego elementu
- FID (First Input Delay) - czas odpowiedzi na pierwszą interakcję
- CLS (Cumulative Layout Shift) - niestabilność layoutu
1. Implementacja Web Vitals w Next.js
1// lib/web-vitals.ts
2import { getCLS, getFID, getFCP, getLCP, getTTFB } from 'web-vitals';
3import * as Sentry from '@sentry/nextjs';
4
5interface WebVitalMetric {
6 name: string;
7 value: number;
8 id: string;
9 delta: number;
10}
11
12// Funkcja do raportowania metryk
13function sendToAnalytics(metric: WebVitalMetric) {
14 // Wyślij do Google Analytics
15 if (typeof window !== 'undefined' && window.gtag) {
16 window.gtag('event', metric.name, {
17 event_category: 'Web Vitals',
18 event_label: metric.id,
19 value: Math.round(metric.name === 'CLS' ? metric.value * 1000 : metric.value),
20 non_interaction: true,
21 });
22 }
23
24 // Wyślij do Sentry
25 Sentry.addBreadcrumb({
26 category: 'web-vital',
27 message: `${metric.name}: ${metric.value}`,
28 level: 'info',
29 data: {
30 name: metric.name,
31 value: metric.value,
32 id: metric.id,
33 delta: metric.delta,
34 },
35 });
36
37 // W przypadku słabych wyników, wyślij jako event do Sentry
38 const thresholds = {
39 LCP: 2500,
40 FID: 100,
41 CLS: 0.1,
42 };
43
44 if (metric.value > thresholds[metric.name as keyof typeof thresholds]) {
45 Sentry.captureMessage(`Poor ${metric.name}: ${metric.value}`, 'warning');
46 }
47}
48
49// Zainicjuj pomiary Web Vitals
50export function initWebVitals() {
51 getCLS(sendToAnalytics);
52 getFID(sendToAnalytics);
53 getFCP(sendToAnalytics);
54 getLCP(sendToAnalytics);
55 getTTFB(sendToAnalytics);
56}2. Komponent do monitoringu Web Vitals
1// components/WebVitalsReporter.tsx
2'use client';
3
4import { useEffect } from 'react';
5import { useReportWebVitals } from 'next/web-vitals';
6
7export default function WebVitalsReporter() {
8 useReportWebVitals((metric) => {
9 // Wyślij do różnych systemów analitycznych
10 switch (metric.name) {
11 case 'FCP':
12 case 'LCP':
13 case 'CLS':
14 case 'FID':
15 case 'TTFB':
16 // Wyślij do własnego API
17 fetch('/api/analytics/web-vitals', {
18 method: 'POST',
19 headers: { 'Content-Type': 'application/json' },
20 body: JSON.stringify(metric),
21 }).catch(console.error);
22 break;
23 default:
24 break;
25 }
26 });
27
28 return null;
29}3. API endpoint do zbierania metryk
1// app/api/analytics/web-vitals/route.ts
2import { NextRequest, NextResponse } from 'next/server';
3import { prisma } from '@/lib/prisma'; // Załóżmy, że używasz Prisma
4
5export async function POST(request: NextRequest) {
6 try {
7 const metric = await request.json();
8
9 // Walidacja podstawowa
10 if (!metric.name || !metric.value) {
11 return NextResponse.json({ error: 'Invalid metric data' }, { status: 400 });
12 }
13
14 // Zapisz do bazy danych
15 await prisma.webVitalMetric.create({
16 data: {
17 name: metric.name,
18 value: metric.value,
19 id: metric.id,
20 delta: metric.delta,
21 url: request.headers.get('referer') || '',
22 userAgent: request.headers.get('user-agent') || '',
23 timestamp: new Date(),
24 },
25 });
26
27 return NextResponse.json({ success: true });
28 } catch (error) {
29 console.error('Error saving web vital metric:', error);
30 return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
31 }
32}Monitoring wydajności z New Relic
New Relic oferuje kompleksowe rozwiązanie do monitoringu wydajności aplikacji Next.js.
1. Konfiguracja New Relic
1npm install newrelic1// newrelic.js (w root projektu)
2'use strict';
3
4exports.config = {
5 app_name: ['Next.js App'],
6 license_key: process.env.NEW_RELIC_LICENSE_KEY,
7
8 // Konfiguracja logowania
9 logging: {
10 level: 'info',
11 },
12
13 // Włącz distributed tracing
14 distributed_tracing: {
15 enabled: true,
16 },
17
18 // Monitoring browserów
19 browser_monitoring: {
20 enable: true,
21 },
22
23 // Konfiguracja specyficzna dla Next.js
24 allow_all_headers: true,
25 attributes: {
26 exclude: [
27 'request.headers.cookie',
28 'request.headers.authorization',
29 'request.headers.x-*',
30 ],
31 },
32};1// next.config.js
2if (process.env.NODE_ENV === 'production') {
3 require('./newrelic');
4}
5
6const nextConfig = {
7 // Twoja konfiguracja Next.js
8};
9
10module.exports = nextConfig;2. Custom instrumentation w New Relic
1// lib/monitoring.ts
2import newrelic from 'newrelic';
3
4// Funkcja do śledzenia niestandardowych metryk
5export function recordCustomMetric(name: string, value: number) {
6 if (process.env.NODE_ENV === 'production') {
7 newrelic.recordMetric(name, value);
8 }
9}
10
11// Funkcja do śledzenia eventów biznesowych
12export function recordCustomEvent(eventType: string, attributes: Record<string, any>) {
13 if (process.env.NODE_ENV === 'production') {
14 newrelic.recordCustomEvent(eventType, attributes);
15 }
16}
17
18// Wrapper dla API calls z automatycznym śledzeniem
19export async function instrumentedApiCall<T>(
20 name: string,
21 apiCall: () => Promise<T>
22): Promise<T> {
23 const startTime = Date.now();
24
25 try {
26 const result = await apiCall();
27
28 // Zapisz metryki sukcesu
29 recordCustomMetric(`Custom/API/${name}/Duration`, Date.now() - startTime);
30 recordCustomEvent('APICall', {
31 name,
32 status: 'success',
33 duration: Date.now() - startTime,
34 });
35
36 return result;
37 } catch (error) {
38 // Zapisz metryki błędu
39 recordCustomMetric(`Custom/API/${name}/Error`, 1);
40 recordCustomEvent('APICall', {
41 name,
42 status: 'error',
43 duration: Date.now() - startTime,
44 error: error.message,
45 });
46
47 throw error;
48 }
49}Vercel Analytics - Monitoring nowej generacji w Metropolii Quantum
W Metropolii Quantum 2150, każda interakcja użytkownika jest monitorowana przez zaawansowane systemy analityczne. Vercel Analytics to natywne rozwiązanie monitoringu dla aplikacji Next.js, zaprojektowane z myślą o prywatności użytkowników i zgodności z GDPR. W przeciwieństwie do Google Analytics, Vercel Analytics działa bezpośrednio w Vercel Edge Network, co zapewnia minimalne opóźnienia i dokładne pomiary.
Vercel Analytics vs Google Analytics
Zanim zdecydujesz się na wybór narzędzia analitycznego, warto zrozumieć kluczowe różnice:
Vercel Analytics:
- Zintegrowany z Vercel Edge Network
- Automatyczne śledzenie Core Web Vitals
- Privacy-first (bez cookies, zgodny z GDPR)
- Zerowa konfiguracja dla podstawowych metryk
- Real-time data w dashboard Vercel
- Płatny plan dla większej liczby page views
- Podstawowe funkcje segmentacji
Google Analytics 4:
- Zaawansowana segmentacja użytkowników
- Funnel analysis i conversion tracking
- Integracja z Google Ads i innymi narzędziami Google
- Darmowy plan dla większości przypadków użycia
- Wymaga zgody na cookies (GDPR compliance)
- Większy wpływ na wydajność strony
- Bardziej skomplikowana konfiguracja
Real User Monitoring (RUM) z Vercel Analytics
Real User Monitoring to technika zbierania danych o rzeczywistych doświadczeniach użytkowników, a nie syntetycznych testów. Vercel Analytics automatycznie śledzi wszystkie kluczowe metryki wydajności.
Instalacja Vercel Analytics
1npm install @vercel/analytics
2# lub
3yarn add @vercel/analytics
4# lub
5pnpm add @vercel/analyticsPodstawowa konfiguracja
1// app/layout.tsx
2import { Analytics } from '@vercel/analytics/react';
3
4export default function RootLayout({
5 children
6}: {
7 children: React.ReactNode
8}) {
9 return (
10 <html lang="pl">
11 <body>
12 {children}
13 <Analytics />
14 </body>
15 </html>
16 );
17}To wszystko! Vercel Analytics automatycznie rozpocznie śledzenie:
- Page views
- Unique visitors
- Top pages
- Top referrers
- Devices i browsers
- Geographic data
Core Web Vitals Tracking
Core Web Vitals to kluczowe metryki wydajności web, które Google używa jako sygnały rankingowe. Vercel Analytics automatycznie śledzi wszystkie trzy metryki:
1. Largest Contentful Paint (LCP)
- Mierzy czas ładowania największego elementu content
- Target: < 2.5s
- Wpływa na: First impression, perceived performance
2. First Input Delay (FID) / Interaction to Next Paint (INP)
- Mierzy responsywność na interakcje użytkownika
- FID target: < 100ms
- INP target: < 200ms
- Wpływa na: Interaktywność, user experience
3. Cumulative Layout Shift (CLS)
- Mierzy stabilność wizualną (niespodziewane przesunięcia layoutu)
- Target: < 0.1
- Wpływa na: Visual stability, user frustration
Dostęp do Core Web Vitals w kodzie
Możesz programatically dostać się do metryk Web Vitals:
1// app/web-vitals.tsx
2'use client';
3
4import { useReportWebVitals } from 'next/web-vitals';
5
6export function WebVitals() {
7 useReportWebVitals((metric) => {
8 console.log(metric);
9
10 // Możesz wysłać metryki do własnego endpointu
11 if (metric.label === 'web-vital') {
12 fetch('/api/analytics/web-vitals', {
13 method: 'POST',
14 body: JSON.stringify(metric),
15 headers: {
16 'Content-Type': 'application/json'
17 }
18 });
19 }
20 });
21
22 return null;
23}1// app/layout.tsx
2import { Analytics } from '@vercel/analytics/react';
3import { WebVitals } from './web-vitals';
4
5export default function RootLayout({ children }: { children: React.ReactNode }) {
6 return (
7 <html lang="pl">
8 <body>
9 {children}
10 <Analytics />
11 <WebVitals />
12 </body>
13 </html>
14 );
15}Custom Events - śledzenie niestandardowych interakcji
Vercel Analytics pozwala śledzić custom events dla specyficznych akcji użytkowników:
1'use client';
2
3import { track } from '@vercel/analytics';
4
5export default function NewsletterForm() {
6 const handleSubscribe = async (email: string) => {
7 try {
8 await subscribeToNewsletter(email);
9
10 // Śledź udaną subskrypcję
11 track('Newsletter Subscribe', {
12 email_domain: email.split('@')[1],
13 source: 'homepage'
14 });
15
16 } catch (error) {
17 // Śledź błąd
18 track('Newsletter Subscribe Error', {
19 error: error.message
20 });
21 }
22 };
23
24 return (
25 <form onSubmit={(e) => {
26 e.preventDefault();
27 const email = e.target.email.value;
28 handleSubscribe(email);
29 }}>
30 <input type="email" name="email" required />
31 <button type="submit">Subskrybuj</button>
32 </form>
33 );
34}Zaawansowane custom events
1'use client';
2
3import { track } from '@vercel/analytics';
4
5export default function ProductPage({ product }: { product: Product }) {
6 // Śledź wyświetlenie produktu
7 useEffect(() => {
8 track('Product View', {
9 product_id: product.id,
10 product_name: product.name,
11 product_category: product.category,
12 product_price: product.price
13 });
14 }, [product]);
15
16 // Śledź dodanie do koszyka
17 const handleAddToCart = () => {
18 track('Add to Cart', {
19 product_id: product.id,
20 quantity: 1,
21 value: product.price
22 });
23
24 addToCart(product);
25 };
26
27 // Śledź rozpoczęcie checkout
28 const handleCheckout = () => {
29 track('Begin Checkout', {
30 value: cartTotal,
31 items: cartItems.length
32 });
33
34 router.push('/checkout');
35 };
36
37 return (
38 <div>
39 <h1>{product.name}</h1>
40 <p>{product.price} PLN</p>
41 <button onClick={handleAddToCart}>Dodaj do koszyka</button>
42 <button onClick={handleCheckout}>Kup teraz</button>
43 </div>
44 );
45}Vercel Speed Insights - monitoring wydajności w czasie rzeczywistym
Speed Insights to rozszerzenie Analytics, które dostarcza szczegółowe informacje o wydajności aplikacji dla prawdziwych użytkowników.
Instalacja
1npm install @vercel/speed-insightsKonfiguracja
1// app/layout.tsx
2import { Analytics } from '@vercel/analytics/react';
3import { SpeedInsights } from '@vercel/speed-insights/next';
4
5export default function RootLayout({ children }: { children: React.ReactNode }) {
6 return (
7 <html lang="pl">
8 <body>
9 {children}
10 <Analytics />
11 <SpeedInsights />
12 </body>
13 </html>
14 );
15}Co śledzi Speed Insights?
Performance Score
- Overall performance rating (0-100)
- Based on Core Web Vitals i inne metryki
Field Data (RUM)
- Rzeczywiste dane od użytkowników
- Segmentowane po device type, connection speed, geographic location
Lab Data
- Syntetyczne testy wydajności
- Kontrolowane warunki dla consistency
Opportunities
- Konkretne sugestie optymalizacji
- Estimated time savings
Kombinacja z innymi narzędziami analitycznymi
Możesz łączyć Vercel Analytics z innymi narzędziami dla pełniejszego obrazu:
1// app/layout.tsx
2import { Analytics } from '@vercel/analytics/react';
3import { SpeedInsights } from '@vercel/speed-insights/next';
4import { GoogleAnalytics } from '@next/third-parties/google';
5
6export default function RootLayout({ children }: { children: React.ReactNode }) {
7 return (
8 <html lang="pl">
9 <body>
10 {children}
11
12 {/* Vercel Analytics - dla Core Web Vitals i podstawowych metryk */}
13 <Analytics />
14 <SpeedInsights />
15
16 {/* Google Analytics - dla zaawansowanej segmentacji */}
17 <GoogleAnalytics gaId={process.env.NEXT_PUBLIC_GA_ID} />
18 </body>
19 </html>
20 );
21}Best practices dla Vercel Analytics
1. Używaj environment-specific tracking
1// app/layout.tsx
2import { Analytics } from '@vercel/analytics/react';
3
4export default function RootLayout({ children }: { children: React.ReactNode }) {
5 return (
6 <html lang="pl">
7 <body>
8 {children}
9
10 {/* Tylko w produkcji */}
11 {process.env.NODE_ENV === 'production' && <Analytics />}
12 </body>
13 </html>
14 );
15}2. Grupuj custom events logicznie
1// lib/analytics.ts
2import { track } from '@vercel/analytics';
3
4export const analytics = {
5 // User events
6 user: {
7 signup: (method: string) => track('User Signup', { method }),
8 login: (method: string) => track('User Login', { method }),
9 logout: () => track('User Logout')
10 },
11
12 // E-commerce events
13 ecommerce: {
14 viewProduct: (productId: string) =>
15 track('Product View', { product_id: productId }),
16 addToCart: (productId: string, value: number) =>
17 track('Add to Cart', { product_id: productId, value }),
18 purchase: (orderId: string, value: number) =>
19 track('Purchase', { order_id: orderId, value })
20 },
21
22 // Engagement events
23 engagement: {
24 shareContent: (contentType: string, method: string) =>
25 track('Share', { content_type: contentType, method }),
26 search: (query: string) =>
27 track('Search', { search_term: query }),
28 playVideo: (videoId: string) =>
29 track('Video Play', { video_id: videoId })
30 }
31};3. Monitoruj conversion funnel
1'use client';
2
3import { track } from '@vercel/analytics';
4
5export default function CheckoutFlow() {
6 // Step 1: View cart
7 useEffect(() => {
8 track('Checkout Step 1 - Cart', {
9 items: cart.length,
10 value: cartTotal
11 });
12 }, []);
13
14 // Step 2: Shipping info
15 const handleShippingSubmit = () => {
16 track('Checkout Step 2 - Shipping', {
17 value: cartTotal
18 });
19 };
20
21 // Step 3: Payment
22 const handlePaymentSubmit = () => {
23 track('Checkout Step 3 - Payment', {
24 value: cartTotal,
25 payment_method: selectedPaymentMethod
26 });
27 };
28
29 // Step 4: Confirmation
30 const handleOrderComplete = (orderId: string) => {
31 track('Purchase Complete', {
32 order_id: orderId,
33 value: cartTotal,
34 items: cart.length
35 });
36 };
37
38 return <CheckoutSteps />;
39}Dashboard i raporty
Vercel Analytics Dashboard oferuje:
Overview
- Total page views
- Unique visitors
- Top pages
- Top referrers
Audiences
- Device breakdown (mobile, desktop, tablet)
- Browser usage
- Operating systems
- Geographic distribution
Real-time
- Live visitor count
- Current popular pages
- Recent events
Web Vitals
- LCP, FID/INP, CLS scores
- Performance over time
- Device-specific metrics
Custom Events
- Event counts
- Event properties
- Conversion tracking
Limitacje i pricing
Free tier:
- 2,500 events/month
- Podstawowe analytics
- 1 member
Pro tier ($10/member/month):
- 100,000 events/month
- Advanced filtering
- Unlimited members
- Data export
Enterprise:
- Custom event limits
- Dedicated support
- SLA guarantees
Integracja z Vercel Edge Config
Możesz dynamicznie kontrolować feature flags bazując na analytics:
1// lib/feature-flags.ts
2import { get } from '@vercel/edge-config';
3import { track } from '@vercel/analytics';
4
5export async function checkFeatureFlag(flagName: string): Promise<boolean> {
6 const isEnabled = await get(flagName);
7
8 // Śledź użycie feature flag
9 if (isEnabled) {
10 track('Feature Flag Enabled', { flag: flagName });
11 }
12
13 return isEnabled || false;
14}Podsumowanie Vercel Analytics
W Metropolii Quantum 2150, gdzie każda milisekunda ma znaczenie, Vercel Analytics dostarcza kwantowej precyzji w monitoringu aplikacji Next.js:
Kluczowe zalety:
- Zero-config setup dla podstawowych metryk
- Automatyczne śledzenie Core Web Vitals
- Privacy-first approach (GDPR compliant)
- Real-time data w Vercel Dashboard
- Integracja z Vercel Edge Network
- Custom events dla business metrics
Kiedy używać Vercel Analytics:
- Aplikacje hostowane na Vercel
- Potrzebujesz privacy-first analytics
- Chcesz zero-config Web Vitals monitoring
- Podstawowa analityka wystarczy dla Twojego case'u
- Budżet pozwala na płatny plan dla większego traffic
Kiedy rozważyć alternatywy:
- Potrzebujesz zaawansowanej segmentacji użytkowników
- Wymagasz funnel analysis i attribution modeling
- Masz bardzo duży traffic (> 1M page views/month)
- Potrzebujesz integracji z ad platforms
- Preferujesz self-hosted solution
Vercel Analytics to idealne rozwiązanie dla większości aplikacji Next.js, oferujące doskonałą równowagę między prostotą, wydajnością i funkcjonalnością. W połączeniu z Speed Insights tworzy kompletny system monitoringu wydajności aplikacji w czasie rzeczywistym!
Monitoring niestandardowy z Prometheus i Grafana
Dla bardziej zaawansowanego monitoringu możesz skonfigurować własny stack z Prometheus i Grafana.
1. Konfiguracja metryk Prometheus
1// lib/metrics.ts
2import client from 'prom-client';
3
4// Rejestr metryk
5const register = new client.Registry();
6
7// Metryki HTTP
8const httpRequestDuration = new client.Histogram({
9 name: 'http_request_duration_seconds',
10 help: 'Duration of HTTP requests in seconds',
11 labelNames: ['method', 'route', 'status_code'],
12 buckets: [0.1, 0.3, 0.5, 0.7, 1, 3, 5, 7, 10],
13});
14
15const httpRequestTotal = new client.Counter({
16 name: 'http_requests_total',
17 help: 'Total number of HTTP requests',
18 labelNames: ['method', 'route', 'status_code'],
19});
20
21// Metryki biznesowe
22const userRegistrations = new client.Counter({
23 name: 'user_registrations_total',
24 help: 'Total number of user registrations',
25});
26
27const ordersTotal = new client.Counter({
28 name: 'orders_total',
29 help: 'Total number of orders',
30 labelNames: ['status'],
31});
32
33// Zarejestruj metryki
34register.registerMetric(httpRequestDuration);
35register.registerMetric(httpRequestTotal);
36register.registerMetric(userRegistrations);
37register.registerMetric(ordersTotal);
38
39// Dodaj domyślne metryki Node.js
40client.collectDefaultMetrics({ register });
41
42export {
43 register,
44 httpRequestDuration,
45 httpRequestTotal,
46 userRegistrations,
47 orderTotal
48};2. Middleware do śledzenia HTTP
1// middleware.ts
2import { NextRequest, NextResponse } from 'next/server';
3import { httpRequestDuration, httpRequestTotal } from '@/lib/metrics';
4
5export function middleware(request: NextRequest) {
6 const start = Date.now();
7
8 // Kontynuuj przetwarzanie żądania
9 const response = NextResponse.next();
10
11 // Zapisz metryki po przetworzeniu żądania
12 response.headers.set('x-response-time', (Date.now() - start).toString());
13
14 // Async zapisanie metryk (nie blokuje odpowiedzi)
15 setImmediate(() => {
16 const duration = (Date.now() - start) / 1000;
17 const labels = {
18 method: request.method,
19 route: request.nextUrl.pathname,
20 status_code: response.status.toString(),
21 };
22
23 httpRequestDuration.observe(labels, duration);
24 httpRequestTotal.inc(labels);
25 });
26
27 return response;
28}
29
30export const config = {
31 matcher: [
32 '/((?!api|_next/static|_next/image|favicon.ico).*)',
33 ],
34};3. Endpoint dla metryk Prometheus
1// app/api/metrics/route.ts
2import { NextRequest, NextResponse } from 'next/server';
3import { register } from '@/lib/metrics';
4
5export async function GET(request: NextRequest) {
6 const metrics = await register.metrics();
7
8 return new NextResponse(metrics, {
9 headers: {
10 'Content-Type': register.contentType,
11 },
12 });
13}Alerting i powiadomienia
Ważnym aspektem monitoringu jest automatyczne powiadamianie o problemach.
1. Integracja ze Slack
1// lib/alerts.ts
2import { IncomingWebhook } from '@slack/webhook';
3
4const webhook = new IncomingWebhook(process.env.SLACK_WEBHOOK_URL);
5
6export async function sendSlackAlert(message: string, severity: 'info' | 'warning' | 'error') {
7 const colors = {
8 info: '#36a64f',
9 warning: '#ffcc00',
10 error: '#ff0000',
11 };
12
13 try {
14 await webhook.send({
15 attachments: [
16 {
17 color: colors[severity],
18 fields: [
19 {
20 title: 'Alert',
21 value: message,
22 short: false,
23 },
24 {
25 title: 'Time',
26 value: new Date().toISOString(),
27 short: true,
28 },
29 {
30 title: 'Environment',
31 value: process.env.NODE_ENV,
32 short: true,
33 },
34 ],
35 },
36 ],
37 });
38 } catch (error) {
39 console.error('Failed to send Slack alert:', error);
40 }
41}
42
43// Funkcja do sprawdzania thresholdów i wysyłania alertów
44export function checkPerformanceThresholds(metrics: {
45 lcp: number;
46 fid: number;
47 cls: number;
48}) {
49 if (metrics.lcp > 2500) {
50 sendSlackAlert(`High LCP detected: ${metrics.lcp}ms`, 'warning');
51 }
52
53 if (metrics.fid > 100) {
54 sendSlackAlert(`High FID detected: ${metrics.fid}ms`, 'warning');
55 }
56
57 if (metrics.cls > 0.1) {
58 sendSlackAlert(`High CLS detected: ${metrics.cls}`, 'warning');
59 }
60}2. Monitoring uptime
1// lib/uptime-monitor.ts
2import { sendSlackAlert } from './alerts';
3
4interface HealthCheckResult {
5 service: string;
6 status: 'healthy' | 'unhealthy';
7 responseTime: number;
8 timestamp: Date;
9}
10
11export async function checkServiceHealth(url: string, service: string): Promise<HealthCheckResult> {
12 const start = Date.now();
13
14 try {
15 const response = await fetch(url, {
16 method: 'GET',
17 timeout: 10000, // 10 second timeout
18 });
19
20 const responseTime = Date.now() - start;
21
22 if (response.ok) {
23 return {
24 service,
25 status: 'healthy',
26 responseTime,
27 timestamp: new Date(),
28 };
29 } else {
30 await sendSlackAlert(`Service ${service} returned ${response.status}`, 'error');
31 return {
32 service,
33 status: 'unhealthy',
34 responseTime,
35 timestamp: new Date(),
36 };
37 }
38 } catch (error) {
39 await sendSlackAlert(`Service ${service} is unreachable: ${error.message}`, 'error');
40 return {
41 service,
42 status: 'unhealthy',
43 responseTime: Date.now() - start,
44 timestamp: new Date(),
45 };
46 }
47}
48
49// Funkcja do uruchamiania health checków
50export async function runHealthChecks() {
51 const services = [
52 { name: 'Main App', url: 'https://your-app.com/api/health' },
53 { name: 'Database', url: 'https://your-app.com/api/health/db' },
54 { name: 'External API', url: 'https://api.external-service.com/health' },
55 ];
56
57 const results = await Promise.allSettled(
58 services.map(service => checkServiceHealth(service.url, service.name))
59 );
60
61 results.forEach((result, index) => {
62 if (result.status === 'fulfilled') {
63 console.log(`Health check for ${services[index].name}:`, result.value);
64 } else {
65 console.error(`Health check failed for ${services[index].name}:`, result.reason);
66 }
67 });
68}Dashboard i wizualizacja danych
1. Prosty dashboard w Next.js
1// app/admin/monitoring/page.tsx
2import { getWebVitalMetrics, getErrorMetrics } from '@/lib/analytics';
3import { LineChart, BarChart } from '@/components/Charts';
4
5export default async function MonitoringDashboard() {
6 const webVitals = await getWebVitalMetrics();
7 const errors = await getErrorMetrics();
8
9 return (
10 <div className="p-6">
11 <h1 className="text-3xl font-bold mb-8">Monitoring Dashboard</h1>
12
13 <div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
14 {/* Core Web Vitals */}
15 <div className="bg-white p-6 rounded-lg shadow">
16 <h2 className="text-xl font-semibold mb-4">Core Web Vitals</h2>
17 <LineChart
18 data={webVitals}
19 categories={['lcp', 'fid', 'cls']}
20 xKey="timestamp"
21 />
22 </div>
23
24 {/* Error Rate */}
25 <div className="bg-white p-6 rounded-lg shadow">
26 <h2 className="text-xl font-semibold mb-4">Error Rate</h2>
27 <BarChart
28 data={errors}
29 category="count"
30 xKey="hour"
31 />
32 </div>
33 </div>
34
35 {/* Real-time metrics */}
36 <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
37 <MetricCard title="Active Users" value="1,234" change="+5.2%" />
38 <MetricCard title="Response Time" value="156ms" change="-2.1%" />
39 <MetricCard title="Error Rate" value="0.01%" change="-50%" />
40 <MetricCard title="Uptime" value="99.9%" change="+0.1%" />
41 </div>
42 </div>
43 );
44}
45
46function MetricCard({ title, value, change }: {
47 title: string;
48 value: string;
49 change: string;
50}) {
51 const isPositive = change.startsWith('+');
52 const isNegative = change.startsWith('-');
53
54 return (
55 <div className="bg-white p-4 rounded-lg shadow">
56 <h3 className="text-sm font-medium text-gray-500">{title}</h3>
57 <div className="mt-2 flex items-baseline">
58 <p className="text-2xl font-semibold text-gray-900">{value}</p>
59 <p className={`ml-2 text-sm font-medium ${
60 isPositive ? 'text-green-600' : isNegative ? 'text-red-600' : 'text-gray-500'
61 }`}>
62 {change}
63 </p>
64 </div>
65 </div>
66 );
67}Najlepsze praktyki monitoringu
1. Strategie sampling
1// lib/sampling.ts
2
3// Funkcja do określania, czy zdarzenie powinno być śledzone
4export function shouldSample(sampleRate: number): boolean {
5 return Math.random() < sampleRate;
6}
7
8// Adaptive sampling - więcej próbek dla błędów
9export function adaptiveSample(isError: boolean): boolean {
10 return isError ? shouldSample(1.0) : shouldSample(0.1);
11}
12
13// Sampling based on user tier
14export function userTierSample(userTier: 'free' | 'premium' | 'enterprise'): boolean {
15 const rates = {
16 free: 0.05, // 5% dla darmowych użytkowników
17 premium: 0.25, // 25% dla premium
18 enterprise: 1.0, // 100% dla enterprise
19 };
20
21 return shouldSample(rates[userTier]);
22}2. Optymalizacja wydajności monitoringu
1// lib/performance-monitoring.ts
2
3// Buffer dla metryk - zbieraj i wysyłaj partiami
4class MetricsBuffer {
5 private buffer: any[] = [];
6 private maxSize = 100;
7 private flushInterval = 30000; // 30 sekund
8
9 constructor() {
10 // Automatyczne opróżnianie bufora
11 setInterval(() => this.flush(), this.flushInterval);
12 }
13
14 add(metric: any) {
15 this.buffer.push(metric);
16
17 if (this.buffer.length >= this.maxSize) {
18 this.flush();
19 }
20 }
21
22 private async flush() {
23 if (this.buffer.length === 0) return;
24
25 const metricsToSend = [...this.buffer];
26 this.buffer = [];
27
28 try {
29 await fetch('/api/metrics/batch', {
30 method: 'POST',
31 headers: { 'Content-Type': 'application/json' },
32 body: JSON.stringify({ metrics: metricsToSend }),
33 });
34 } catch (error) {
35 console.error('Failed to send metrics batch:', error);
36 // Wróć metryki do bufora w przypadku błędu
37 this.buffer.unshift(...metricsToSend);
38 }
39 }
40}
41
42export const metricsBuffer = new MetricsBuffer();Podsumowanie
Monitoring wydajności i analityka błędów to kluczowe elementy każdej produkcyjnej aplikacji Next.js. Kompleksowe podejście do monitoringu obejmuje:
- Śledzenie błędów - automatyczne wykrywanie i raportowanie problemów
- Monitoring wydajności - Core Web Vitals i inne kluczowe metryki
- Alerting - natychmiastowe powiadomienia o krytycznych problemach
- Dashboardy - wizualizacja danych dla podejmowania decyzji
- Analityka użytkowników - zrozumienie, jak użytkownicy korzystają z aplikacji
Wybierając narzędzia monitoringu, rozważ:
- Sentry - dla kompleksowego śledzenia błędów
- Vercel Analytics - dla prostego monitoringu na Vercel
- New Relic/DataDog - dla zaawansowanego monitoringu wydajności
- Własne rozwiązania - dla specjalistycznych potrzeb
Pamiętaj o balansie między głębokością monitoringu a wpływem na wydajność aplikacji. Używaj sampling, bufferingu i optymalizuj zapytania, aby monitoring nie wpływał negatywnie na doświadczenie użytkownika.
Kod do tej lekcji: App.tsx
1import React, { useState, useEffect } from 'react';
2
3// Demo: Monitoring wydajnosci i analityka bledow
4interface ErrorEntry {
5 id: string;
6 type: 'error' | 'warning' | 'info';
7 message: string;
8 stack: string;
9 count: number;
10 lastSeen: string;
11 status: 'new' | 'acknowledged' | 'resolved';
12}
13
14interface PerformanceMetric {
15 name: string;
16 value: number;
17 unit: string;
18 status: 'good' | 'needs-improvement' | 'poor';
19 threshold: { good: number; poor: number };
20}
21
22function MonitoringDemo() {
23 const [errors, setErrors] = useState<ErrorEntry[]>([
24 { id: '1', type: 'error', message: 'TypeError: Cannot read property of undefined', stack: 'at UserProfile (/app/components/UserProfile.tsx:42)', count: 23, lastSeen: '2 min ago', status: 'new' },
25 { id: '2', type: 'error', message: 'NetworkError: Failed to fetch /api/data', stack: 'at fetchData (/app/lib/api.ts:15)', count: 8, lastSeen: '15 min ago', status: 'acknowledged' },
26 { id: '3', type: 'warning', message: 'Large bundle size: chunk-vendors.js (450KB)', stack: 'Build warning', count: 1, lastSeen: '1 hour ago', status: 'new' },
27 { id: '4', type: 'error', message: 'Hydration mismatch in <Header />', stack: 'at Header (/app/components/Header.tsx:18)', count: 156, lastSeen: '30 sec ago', status: 'new' },
28 ]);
29
30 const [metrics, setMetrics] = useState<PerformanceMetric[]>([
31 { name: 'FCP', value: 1.2, unit: 's', status: 'good', threshold: { good: 1.8, poor: 3.0 } },
32 { name: 'LCP', value: 2.4, unit: 's', status: 'needs-improvement', threshold: { good: 2.5, poor: 4.0 } },
33 { name: 'CLS', value: 0.05, unit: '', status: 'good', threshold: { good: 0.1, poor: 0.25 } },
34 { name: 'FID', value: 45, unit: 'ms', status: 'good', threshold: { good: 100, poor: 300 } },
35 { name: 'TTFB', value: 320, unit: 'ms', status: 'needs-improvement', threshold: { good: 200, poor: 600 } },
36 { name: 'INP', value: 180, unit: 'ms', status: 'needs-improvement', threshold: { good: 200, poor: 500 } },
37 ]);
38
39 const [activeTab, setActiveTab] = useState<'errors' | 'performance' | 'setup'>('errors');
40 const [selectedTool, setSelectedTool] = useState<'sentry' | 'vercel' | 'custom'>('sentry');
41
42 const statusColors: Record<string, string> = { good: '#10b981', 'needs-improvement': '#f59e0b', poor: '#ef4444' };
43 const errorTypeColors: Record<string, string> = { error: '#ef4444', warning: '#f59e0b', info: '#3b82f6' };
44 const errorStatusColors: Record<string, string> = { new: '#ef4444', acknowledged: '#f59e0b', resolved: '#10b981' };
45
46 const updateErrorStatus = (id: string, status: ErrorEntry['status']) => {
47 setErrors(prev => prev.map(e => e.id === id ? { ...e, status } : e));
48 };
49
50 const triggerError = () => {
51 const newError: ErrorEntry = {
52 id: String(Date.now()),
53 type: 'error',
54 message: ['RangeError: Maximum call stack', 'SyntaxError: Unexpected token', 'ReferenceError: x is not defined'][Math.floor(Math.random() * 3)],
55 stack: 'at Component (/app/page.tsx:' + Math.floor(Math.random() * 100) + ')',
56 count: 1, lastSeen: 'just now', status: 'new',
57 };
58 setErrors(prev => [newError, ...prev]);
59 };
60
61 const setupCode: Record<string, string> = {
62 sentry: "// sentry.client.config.ts\nimport * as Sentry from '@sentry/nextjs';\n\nSentry.init({\n dsn: process.env.SENTRY_DSN,\n tracesSampleRate: 0.1,\n replaysSessionSampleRate: 0.1,\n integrations: [\n Sentry.replayIntegration(),\n Sentry.browserTracingIntegration(),\n ],\n});",
63 vercel: "// next.config.js\nmodule.exports = {\n experimental: {\n instrumentationHook: true,\n },\n};\n\n// instrumentation.ts\nexport async function register() {\n if (process.env.NEXT_RUNTIME === 'nodejs') {\n // Server-side monitoring\n }\n}",
64 custom: "// lib/monitoring.ts\nexport function reportError(error: Error, context?: object) {\n fetch('/api/errors', {\n method: 'POST',\n body: JSON.stringify({\n message: error.message,\n stack: error.stack,\n context,\n timestamp: Date.now(),\n }),\n });\n}",
65 };
66
67 return (
68 <div style={{ background: '#0f0f23', minHeight: '100vh', padding: '20px', color: '#e0e0e0', fontFamily: 'system-ui' }}>
69 <h1 style={{ color: '#64ffda', marginBottom: '8px' }}>Monitoring & Error Tracking</h1>
70 <p style={{ color: '#8892b0', marginBottom: '24px' }}>Monitoring wydajnosci i sledzenie bledow w Next.js</p>
71
72 <div style={{ display: 'flex', gap: '8px', marginBottom: '24px' }}>
73 {[
74 { id: 'errors' as const, label: 'Bledy (' + errors.filter(e => e.status === 'new').length + ')' },
75 { id: 'performance' as const, label: 'Wydajnosc' },
76 { id: 'setup' as const, label: 'Konfiguracja' },
77 ].map(tab => (
78 <button key={tab.id} onClick={() => setActiveTab(tab.id)} style={{
79 padding: '10px 20px', borderRadius: '8px', border: 'none', cursor: 'pointer',
80 background: activeTab === tab.id ? '#64ffda' : '#1a1a2e', color: activeTab === tab.id ? '#0f0f23' : '#8892b0', fontWeight: 'bold',
81 }}>{tab.label}</button>
82 ))}
83 </div>
84
85 {activeTab === 'errors' && (
86 <div>
87 <button onClick={triggerError} style={{ padding: '8px 16px', borderRadius: '6px', background: '#ef4444', color: '#fff', border: 'none', cursor: 'pointer', marginBottom: '16px' }}>
88 Symuluj blad
89 </button>
90 <div style={{ display: 'grid', gap: '8px' }}>
91 {errors.map(err => (
92 <div key={err.id} style={{ padding: '14px', borderRadius: '8px', background: '#1a1a2e', border: '1px solid #333' }}>
93 <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '8px' }}>
94 <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
95 <span style={{ fontSize: '11px', padding: '2px 8px', borderRadius: '12px', background: errorTypeColors[err.type] + '30', color: errorTypeColors[err.type] }}>{err.type}</span>
96 <span style={{ fontSize: '11px', padding: '2px 8px', borderRadius: '12px', background: errorStatusColors[err.status] + '30', color: errorStatusColors[err.status] }}>{err.status}</span>
97 <span style={{ fontSize: '12px', color: '#8892b0' }}>x{err.count} | {err.lastSeen}</span>
98 </div>
99 <select value={err.status} onChange={e => updateErrorStatus(err.id, e.target.value as any)} style={{ padding: '4px 8px', borderRadius: '4px', background: '#0f0f23', color: '#fff', border: '1px solid #333', fontSize: '11px' }}>
100 <option value="new">New</option>
101 <option value="acknowledged">Acknowledged</option>
102 <option value="resolved">Resolved</option>
103 </select>
104 </div>
105 <div style={{ color: errorTypeColors[err.type], fontWeight: 'bold', fontSize: '14px' }}>{err.message}</div>
106 <div style={{ fontSize: '12px', color: '#555', marginTop: '4px' }}>{err.stack}</div>
107 </div>
108 ))}
109 </div>
110 </div>
111 )}
112
113 {activeTab === 'performance' && (
114 <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '16px' }}>
115 {metrics.map(m => (
116 <div key={m.name} style={{ background: '#1a1a2e', padding: '20px', borderRadius: '12px', border: '1px solid #333', textAlign: 'center' }}>
117 <div style={{ fontSize: '32px', fontWeight: 'bold', color: statusColors[m.status] }}>{m.value}{m.unit}</div>
118 <div style={{ fontSize: '16px', color: '#64ffda', marginTop: '4px' }}>{m.name}</div>
119 <div style={{ fontSize: '12px', color: statusColors[m.status], marginTop: '4px' }}>{m.status.replace('-', ' ')}</div>
120 <div style={{ marginTop: '8px', height: '4px', background: '#333', borderRadius: '2px', overflow: 'hidden' }}>
121 <div style={{ width: Math.min(100, (m.value / m.threshold.poor) * 100) + '%', height: '100%', background: statusColors[m.status] }} />
122 </div>
123 <div style={{ fontSize: '10px', color: '#555', marginTop: '4px' }}>Good: <{m.threshold.good}{m.unit} | Poor: >{m.threshold.poor}{m.unit}</div>
124 </div>
125 ))}
126 </div>
127 )}
128
129 {activeTab === 'setup' && (
130 <div>
131 <div style={{ display: 'flex', gap: '8px', marginBottom: '16px' }}>
132 {[
133 { id: 'sentry' as const, label: 'Sentry' },
134 { id: 'vercel' as const, label: 'Vercel Analytics' },
135 { id: 'custom' as const, label: 'Custom' },
136 ].map(tool => (
137 <button key={tool.id} onClick={() => setSelectedTool(tool.id)} style={{
138 padding: '8px 16px', borderRadius: '8px', border: 'none', cursor: 'pointer',
139 background: selectedTool === tool.id ? '#7c3aed' : '#1a1a2e', color: '#fff',
140 }}>{tool.label}</button>
141 ))}
142 </div>
143 <pre style={{ background: '#1a1a2e', padding: '16px', borderRadius: '8px', fontSize: '12px', overflow: 'auto', border: '1px solid #333', color: '#e0e0e0', whiteSpace: 'pre-wrap' }}>
144 {setupCode[selectedTool]}
145 </pre>
146 </div>
147 )}
148 </div>
149 );
150}
151
152export default MonitoringDemo;Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Które metryki Core Web Vitals mierzy Google do oceny wydajności strony?
Zadania praktyczne w grze
- Edytor kodu
Utwórz system monitorowania aplikacji: performance metrics, error tracking, uptime monitoring używając Web Vitals
- Układanie w pionie
Ułóż elementy w prawidłowej kolejności: Choose tool → Add instrumentation → Set
- Układanie w pionie
Ułóż etapy konfiguracji performance budgets w prawidłowej kolejności: