Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ NEXT_PUBLIC_FARO_APP_NAME=next-frontend
NEXT_PUBLIC_FARO_APP_NAMESPACE=nextjs-example
NEXT_PUBLIC_FARO_APP_VERSION=1.0.0
NEXT_PUBLIC_APP_ENV=development
NEXT_PUBLIC_METRICS_ENABLED=false

# Next App BACKEND Instrumentation
## Example assumes that the collector is running on the same machine
Expand Down
24 changes: 24 additions & 0 deletions .env.production
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
NEXT_PUBLIC_API_BASE_URL=http://localhost:3000/v1
PORT=3000
# Next App FRONTEND Instrumentation
NEXT_PUBLIC_FARO_URL=http://localhost:12347/collect
NEXT_PUBLIC_FARO_APP_NAME=ttopen-frontend
NEXT_PUBLIC_FARO_APP_NAMESPACE=ttopen-front
NEXT_PUBLIC_FARO_APP_VERSION=1.0.0
NEXT_PUBLIC_APP_ENV=development
NEXT_PUBLIC_METRICS_ENABLED=true

# Next App BACKEND Instrumentation
## Example assumes that the collector is running on the same machine
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
## Force protobuf
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
## Set Backend service name
OTEL_SERVICE_NAME=next-backend
## Customize resource attributes, namespace is a recommended attribute
OTEL_RESOURCE_ATTRIBUTES=service.namespace=nextjs-ttopen

# OTel collector
GRAFANA_CLOUD_USERNAME=
GRAFANA_CLOUD_API_KEY=
GRAFANA_CLOUD_ENDPOINT=
1 change: 1 addition & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ jobs:
NEXT_PUBLIC_APP_ENV=${{ vars.NEXT_PUBLIC_APP_ENV }}
NEXT_PUBLIC_FARO_APP_NAME=${{ vars.NEXT_PUBLIC_FARO_APP_NAME }}
NEXT_PUBLIC_FARO_APP_NAMESPACE=${{ vars.NEXT_PUBLIC_FARO_APP_NAMESPACE }}
NEXT_PUBLIC_METRICS_ENABLED=${{ vars.NEXT_PUBLIC_METRICS_ENABLED }}
SKIP_ENV_VALIDATION=true
push: ${{ (env.IS_PUSH == 'true' && env.IS_BASE_BRANCH == 'true') ||
env.FORCE_PUSH == 'true' }}
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,5 +82,6 @@ jobs:
NEXT_PUBLIC_APP_ENV: ${{ vars.NEXT_PUBLIC_APP_ENV }}
NEXT_PUBLIC_FARO_APP_NAME: ${{ vars.NEXT_PUBLIC_FARO_APP_NAME }}
NEXT_PUBLIC_FARO_APP_NAMESPACE: ${{ vars.NEXT_PUBLIC_FARO_APP_NAMESPACE }}
NEXT_PUBLIC_METRICS_ENABLED: ${{vars.NEXT_PUBLIC_METRICS_ENABLED}}
# - name: Build Storybook
# run: pnpm build-storybook
1 change: 1 addition & 0 deletions .github/workflows/release-please.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ jobs:
NEXT_PUBLIC_APP_ENV=${{ vars.NEXT_PUBLIC_APP_ENV }}
NEXT_PUBLIC_FARO_APP_NAME=${{ vars.NEXT_PUBLIC_FARO_APP_NAME }}
NEXT_PUBLIC_FARO_APP_NAMESPACE=${{ vars.NEXT_PUBLIC_FARO_APP_NAMESPACE }}
NEXT_PUBLIC_METRICS_ENABLED=${{vars.NEXT_PUBLIC_METRICS_ENABLED}}
SKIP_ENV_VALIDATION=true
push: true
tags: ${{ steps.meta.outputs.tags }}
Expand Down
4 changes: 3 additions & 1 deletion Dockerfile.prod
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ ARG NEXT_PUBLIC_API_BASE_URL
ARG NEXT_PUBLIC_FARO_URL
ARG NEXT_PUBLIC_FARO_APP_VERSION
ARG NEXT_PUBLIC_APP_ENV
ARG NEXT_PUBLIC_METRICS_ENABLED
ARG SKIP_ENV_VALIDATION=false

ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \
Expand All @@ -29,7 +30,8 @@ ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \
NEXT_PUBLIC_FARO_APP_VERSION=$NEXT_PUBLIC_FARO_APP_VERSION \
NEXT_PUBLIC_APP_ENV=$NEXT_PUBLIC_APP_ENV \
SKIP_ENV_VALIDATION=$SKIP_ENV_VALIDATION \
NEXT_TELEMETRY_DISABLED=1
NEXT_TELEMETRY_DISABLED=1 \
NEXT_PUBLIC_METRICS_ENABLED=$NEXT_PUBLIC_METRICS_ENABLED

COPY --from=deps /app/node_modules ./node_modules
COPY . .
Expand Down
50 changes: 50 additions & 0 deletions src/shared/config/env.client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { z } from 'zod/v4';

export const envSchemaClient = z.object({
NEXT_PUBLIC_API_BASE_URL: z.url('NEXT_PUBLIC_API_BASE_URL должен быть валидным URL'),
NEXT_PUBLIC_FARO_URL: z.url(
'NEXT_PUBLIC_FARO_URL должен быть валидным URL (например, http://alloy:12347/collect)'
),
NEXT_PUBLIC_FARO_APP_NAME: z
.string({
error: 'Имя приложения для Faro обязательно',
})
.min(1, 'Имя приложения не может быть пустым'),
NEXT_PUBLIC_FARO_APP_NAMESPACE: z
.string({
error: 'Namespace приложения обязателен',
})
.min(1, 'Namespace не может быть пустым'),
NEXT_PUBLIC_FARO_APP_VERSION: z.string().default('1.0.0'),
NEXT_PUBLIC_APP_ENV: z
.string({
error: 'Окружение (APP_ENV) обязательно',
})
.min(1, 'Окружение не может быть пустым'),
NEXT_PUBLIC_METRICS_ENABLED: z
.enum(['true', 'false'], {
error: 'NEXT_PUBLIC_METRICS_ENABLED - обязателен',
})
.transform((v) => v === 'true'),
});

const _env = envSchemaClient.safeParse({
NEXT_PUBLIC_API_BASE_URL: process.env.NEXT_PUBLIC_API_BASE_URL,
NEXT_PUBLIC_FARO_URL: process.env.NEXT_PUBLIC_FARO_URL,
NEXT_PUBLIC_FARO_APP_NAME: process.env.NEXT_PUBLIC_FARO_APP_NAME,
NEXT_PUBLIC_FARO_APP_NAMESPACE: process.env.NEXT_PUBLIC_FARO_APP_NAMESPACE,
NEXT_PUBLIC_FARO_APP_VERSION: process.env.NEXT_PUBLIC_FARO_APP_VERSION,
NEXT_PUBLIC_APP_ENV: process.env.NEXT_PUBLIC_APP_ENV,
NEXT_PUBLIC_METRICS_ENABLED: process.env.NEXT_PUBLIC_METRICS_ENABLED,
});

if (!_env.success) {
console.error('❌ [ENV VALIDATION ERROR]:', z.treeifyError(_env.error).properties);
if (typeof window === 'undefined') {
throw new Error('Client env validation failed on server-side rendering');
}
}

export type ClientEnv = z.infer<typeof envSchemaClient>;

export const env: ClientEnv = _env.success ? _env.data : ({} as ClientEnv);
14 changes: 9 additions & 5 deletions src/shared/config/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import { z } from 'zod/v4';
const isServer = typeof window === 'undefined';
const isBuild = process.env.SKIP_ENV_VALIDATION === 'true';

const metricEnabledSchema = z.enum(['true', 'false'], {
error: 'NEXT_PUBLIC_METRICS_ENABLED - обязателен',
});

const envSchemaServer = z.object({
NODE_ENV: z
.enum(['development', 'production', 'test'], {
Expand Down Expand Up @@ -35,11 +39,7 @@ const envSchemaServer = z.object({
});

const envSchemaClient = z.object({
NEXT_PUBLIC_API_BASE_URL: z
.string({
error: 'API Base URL обязателен',
})
.url('NEXT_PUBLIC_API_BASE_URL должен быть валидным URL'),
NEXT_PUBLIC_API_BASE_URL: z.url('NEXT_PUBLIC_API_BASE_URL должен быть валидным URL'),
NEXT_PUBLIC_FARO_URL: z
.string({
error: 'URL для Faro (Alloy) обязателен',
Expand All @@ -61,6 +61,10 @@ const envSchemaClient = z.object({
error: 'Окружение (APP_ENV) обязательно',
})
.min(1, 'Окружение не может быть пустым'),
NEXT_PUBLIC_METRICS_ENABLED:
process.env.NODE_ENV === 'development'
? metricEnabledSchema.default('false').transform((v) => v === 'true')
: metricEnabledSchema.transform((v) => v === 'true'),
});

const envSchema = envSchemaClient.extend(envSchemaServer.shape);
Expand Down
2 changes: 2 additions & 0 deletions src/shared/config/metrics/FrontendObservability.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
import { useEffect } from 'react';
import { faro, getWebInstrumentations, initializeFaro } from '@grafana/faro-web-sdk';
import { TracingInstrumentation } from '@grafana/faro-web-tracing';
import { env } from '../env.client';

let isFaroInitialized = false;

export default function FrontendObservability() {
useEffect(() => {
if (!env.NEXT_PUBLIC_METRICS_ENABLED) return;
const initializeWhenIdle = () => {
if (isFaroInitialized || faro.api) {
return;
Expand Down
Loading