Compare commits

...

2 Commits

Author SHA1 Message Date
0a797260e3 feat: Introduce NEXT_PUBLIC_TARGET build argument and abstract server-side error reporting to a dedicated service.
Some checks failed
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 7s
Build & Deploy KLZ Cables / 🏗️ Build App (push) Successful in 4m2s
Build & Deploy KLZ Cables / 🏗️ Build Gatekeeper (push) Successful in 21s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Successful in 11m56s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Successful in 43s
Build & Deploy KLZ Cables / ⚡ PageSpeed (push) Failing after 2m7s
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 1s
2026-02-02 15:27:04 +01:00
2a4cc76292 feat: Disable Lighthouse CI report uploading and generate a local summary table of PageSpeed scores instead. 2026-02-02 15:20:33 +01:00
9 changed files with 115 additions and 75 deletions

View File

@@ -10,6 +10,11 @@
# ────────────────────────────────────────────────────────────────────────────
NODE_ENV=development
NEXT_PUBLIC_BASE_URL=http://localhost:3000
# TARGET is used to differentiate between environments (testing, staging, production)
# NEXT_PUBLIC_TARGET makes this information available to the frontend
NEXT_PUBLIC_TARGET=development
# TARGET is used server-side
TARGET=development
# ────────────────────────────────────────────────────────────────────────────
# Analytics (Umami)

View File

@@ -234,6 +234,7 @@ jobs:
--build-arg NEXT_PUBLIC_BASE_URL="$NEXT_PUBLIC_BASE_URL" \
--build-arg NEXT_PUBLIC_UMAMI_WEBSITE_ID="$NEXT_PUBLIC_UMAMI_WEBSITE_ID" \
--build-arg NEXT_PUBLIC_UMAMI_SCRIPT_URL="$NEXT_PUBLIC_UMAMI_SCRIPT_URL" \
--build-arg NEXT_PUBLIC_TARGET="$TARGET" \
--build-arg DIRECTUS_URL="$DIRECTUS_URL" \
-t registry.infra.mintel.me/mintel/klz-cables.com:$IMAGE_TAG \
--cache-from type=registry,ref=registry.infra.mintel.me/mintel/klz-cables.com:buildcache \
@@ -414,8 +415,8 @@ jobs:
runs-on: docker
container:
image: catthehacker/ubuntu:act-latest
outputs:
report_url: ${{ steps.save.outputs.report_url }}
# outputs:
# report_url: ${{ steps.save.outputs.report_url }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
@@ -498,15 +499,7 @@ jobs:
CHROME_PATH: /usr/bin/chromium
run: npm run pagespeed:test
- name: 💾 Save Report URL
id: save
if: always()
run: |
if [ -f pagespeed-report-url.txt ]; then
URL=$(cat pagespeed-report-url.txt)
echo "report_url=$URL" >> $GITHUB_OUTPUT
echo "✅ Report URL found: $URL"
fi
# ──────────────────────────────────────────────────────────────────────────────
# JOB 6: Notifications
@@ -534,14 +527,9 @@ jobs:
- name: 🔔 Gotify - Success
if: needs.deploy.result == 'success'
run: |
REPORT_MSG=""
if [ -n "${{ needs.pagespeed.outputs.report_url }}" ]; then
REPORT_MSG="\n\n⚡ **PageSpeed Report:**\n${{ needs.pagespeed.outputs.report_url }}"
fi
curl -s -k -X POST "${{ secrets.GOTIFY_URL }}/message?token=${{ secrets.GOTIFY_TOKEN }}" \
-F "title=${{ needs.prepare.outputs.gotify_title }}" \
-F "message=Erfolgreich deployt auf **${{ needs.prepare.outputs.target }}**\n\nVersion: **${{ needs.prepare.outputs.image_tag }}**\nCommit: ${{ needs.prepare.outputs.short_sha }} (${{ needs.prepare.outputs.commit_msg }})\nVon: ${{ github.actor }}\nRun: ${{ github.run_id }}${REPORT_MSG}" \
-F "message=Erfolgreich deployt auf **${{ needs.prepare.outputs.target }}**\n\nVersion: **${{ needs.prepare.outputs.image_tag }}**\nCommit: ${{ needs.prepare.outputs.short_sha }} (${{ needs.prepare.outputs.commit_msg }})\nVon: ${{ github.actor }}\nRun: ${{ github.run_id }}" \
-F "priority=4" || true
- name: 🔔 Gotify - Failure

View File

@@ -27,11 +27,13 @@ ENV NEXT_TELEMETRY_DISABLED=1
ARG NEXT_PUBLIC_BASE_URL
ARG NEXT_PUBLIC_UMAMI_WEBSITE_ID
ARG NEXT_PUBLIC_UMAMI_SCRIPT_URL
ARG NEXT_PUBLIC_TARGET
ARG DIRECTUS_URL
ENV NEXT_PUBLIC_BASE_URL=$NEXT_PUBLIC_BASE_URL
ENV NEXT_PUBLIC_UMAMI_WEBSITE_ID=$NEXT_PUBLIC_UMAMI_WEBSITE_ID
ENV NEXT_PUBLIC_UMAMI_SCRIPT_URL=$NEXT_PUBLIC_UMAMI_SCRIPT_URL
ENV NEXT_PUBLIC_TARGET=$NEXT_PUBLIC_TARGET
ENV DIRECTUS_URL=$DIRECTUS_URL
# Validate environment variables during build

View File

@@ -18,10 +18,10 @@ function createConfig() {
return {
env: env.NODE_ENV,
target,
isProduction: target === 'production',
isProduction: target === 'production' || !target,
isStaging: target === 'staging',
isTesting: target === 'testing',
isDevelopment: target === 'development' || env.NODE_ENV === 'development',
isDevelopment: target === 'development',
baseUrl: env.NEXT_PUBLIC_BASE_URL,

View File

@@ -1,6 +1,6 @@
import { createDirectus, rest, authentication, readItems, readCollections } from '@directus/sdk';
import { config } from './config';
import * as Sentry from '@sentry/nextjs';
import { getServerAppServices } from './services/create-services.server';
const { url, adminEmail, password, token, proxyPath, internalUrl } = config.directus;
@@ -19,7 +19,7 @@ const shouldShowDevErrors = config.isTesting || config.isDevelopment;
*/
function formatError(error: any) {
if (shouldShowDevErrors) {
return error.message || 'An unexpected error occurred.';
return error.errors?.[0]?.message || error.message || 'An unexpected error occurred.';
}
return 'A system error occurred. Our team has been notified.';
}
@@ -33,7 +33,9 @@ export async function ensureAuthenticated() {
try {
await client.login(adminEmail, password);
} catch (e) {
Sentry.captureException(e);
if (typeof window === 'undefined') {
getServerAppServices().errors.captureException(e, { part: 'directus_auth' });
}
console.error('Failed to authenticate with Directus:', e);
}
}
@@ -78,7 +80,9 @@ export async function getProducts(locale: string = 'de') {
);
return items.map((item) => mapDirectusProduct(item, locale));
} catch (error) {
Sentry.captureException(error);
if (typeof window === 'undefined') {
getServerAppServices().errors.captureException(error, { part: 'directus_get_products' });
}
console.error('Error fetching products:', error);
return [];
}
@@ -104,7 +108,12 @@ export async function getProductBySlug(slug: string, locale: string = 'de') {
if (!items || items.length === 0) return null;
return mapDirectusProduct(items[0], locale);
} catch (error) {
Sentry.captureException(error);
if (typeof window === 'undefined') {
getServerAppServices().errors.captureException(error, {
part: 'directus_get_product_by_slug',
slug,
});
}
console.error(`Error fetching product ${slug}:`, error);
return null;
}
@@ -117,7 +126,9 @@ export async function checkHealth() {
await ensureAuthenticated();
await client.request(readCollections());
} catch (e: any) {
Sentry.captureException(e);
if (typeof window === 'undefined') {
getServerAppServices().errors.captureException(e, { part: 'directus_health_auth' });
}
console.error('Directus authentication failed during health check:', e);
return {
status: 'error',
@@ -133,7 +144,9 @@ export async function checkHealth() {
try {
await client.request(readItems('products', { limit: 1 }));
} catch (e: any) {
Sentry.captureException(e);
if (typeof window === 'undefined') {
getServerAppServices().errors.captureException(e, { part: 'directus_health_schema' });
}
if (
e.message?.includes('does not exist') ||
e.code === 'INVALID_PAYLOAD' ||
@@ -142,7 +155,7 @@ export async function checkHealth() {
return {
status: 'error',
message: shouldShowDevErrors
? 'The "products" collection is missing or inaccessible. Please sync your data.'
? `The "products" collection is missing or inaccessible. Error: ${e.message || 'Unknown'}`
: 'Required data structures are currently unavailable.',
code: 'SCHEMA_MISSING',
};
@@ -150,7 +163,7 @@ export async function checkHealth() {
return {
status: 'error',
message: shouldShowDevErrors
? `Schema error: ${e.message}`
? `Schema error: ${e.errors?.[0]?.message || e.message || 'Unknown error'}`
: 'The data schema is currently misconfigured.',
code: 'SCHEMA_ERROR',
};
@@ -158,7 +171,9 @@ export async function checkHealth() {
return { status: 'ok', message: 'Directus is reachable and responding.' };
} catch (error: any) {
Sentry.captureException(error);
if (typeof window === 'undefined') {
getServerAppServices().errors.captureException(error, { part: 'directus_health_critical' });
}
console.error('Directus health check failed with unexpected error:', error);
return {
status: 'error',

View File

@@ -11,9 +11,7 @@ const preprocessEmptyString = (val: unknown) => (val === '' ? undefined : val);
export const envSchema = z.object({
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
NEXT_PUBLIC_BASE_URL: z.preprocess(preprocessEmptyString, z.string().url()),
NEXT_PUBLIC_TARGET: z
.enum(['development', 'testing', 'staging', 'production'])
.default('development'),
NEXT_PUBLIC_TARGET: z.enum(['development', 'testing', 'staging', 'production']).optional(),
// Analytics
NEXT_PUBLIC_UMAMI_WEBSITE_ID: z.preprocess(preprocessEmptyString, z.string().optional()),
@@ -50,7 +48,7 @@ export const envSchema = z.object({
INTERNAL_DIRECTUS_URL: z.preprocess(preprocessEmptyString, z.string().url().optional()),
// Deploy Target
TARGET: z.enum(['development', 'testing', 'staging', 'production']).default('development'),
TARGET: z.enum(['development', 'testing', 'staging', 'production']).optional(),
});
export type Env = z.infer<typeof envSchema>;

View File

@@ -13,7 +13,7 @@ export function getServerAppServices(): AppServices {
// Create logger first to log initialization
const logger = new PinoLoggerService('server');
logger.info('Initializing server application services', {
environment: getMaskedConfig(),
timestamp: new Date().toISOString(),
@@ -40,7 +40,9 @@ export function getServerAppServices(): AppServices {
: new NoopErrorReportingService();
if (config.errors.glitchtip.enabled) {
logger.info('GlitchTip error reporting service initialized');
logger.info('GlitchTip error reporting service initialized', {
dsnPresent: Boolean(config.errors.glitchtip.dsn),
});
} else {
logger.info('Noop error reporting service initialized (error reporting disabled)');
}
@@ -54,9 +56,8 @@ export function getServerAppServices(): AppServices {
});
singleton = new AppServices(analytics, errors, cache, logger);
logger.info('All application services initialized successfully');
return singleton;
}

View File

@@ -1,22 +1,19 @@
module.exports = {
ci: {
collect: {
numberOfRuns: 1,
settings: {
preset: 'desktop',
onlyCategories: ['performance', 'accessibility', 'best-practices', 'seo'],
},
},
assert: {
assertions: {
'categories:performance': ['warn', { minScore: 0.9 }],
'categories:accessibility': ['warn', { minScore: 0.9 }],
'categories:best-practices': ['warn', { minScore: 0.9 }],
'categories:seo': ['warn', { minScore: 0.9 }],
},
},
upload: {
target: 'temporary-public-storage',
},
ci: {
collect: {
numberOfRuns: 1,
settings: {
preset: 'desktop',
onlyCategories: ['performance', 'accessibility', 'best-practices', 'seo'],
},
},
assert: {
assertions: {
'categories:performance': ['warn', { minScore: 0.9 }],
'categories:accessibility': ['warn', { minScore: 0.9 }],
'categories:best-practices': ['warn', { minScore: 0.9 }],
'categories:seo': ['warn', { minScore: 0.9 }],
},
},
},
};

View File

@@ -79,33 +79,67 @@ async function main() {
const chromePath = process.env.CHROME_PATH || process.env.PUPPETEER_EXECUTABLE_PATH;
const chromePathArg = chromePath ? `--collect.chromePath="${chromePath}"` : '';
// Clean up old reports
if (fs.existsSync('.lighthouseci')) {
fs.rmSync('.lighthouseci', { recursive: true, force: true });
}
// Using a more robust way to execute and capture output
const lhciCommand = `npx lhci collect ${urlArgs} ${chromePathArg} --collect.settings.chromeFlags='--no-sandbox --disable-setuid-sandbox' --collect.settings.extraHeaders='${extraHeaders}' && npx lhci assert && npx lhci upload`;
// We remove 'npx lhci upload' to keep everything local and avoid Google-hosted reports
const lhciCommand = `npx lhci collect ${urlArgs} ${chromePathArg} --collect.settings.chromeFlags='--no-sandbox --disable-setuid-sandbox' --collect.settings.extraHeaders='${extraHeaders}' && npx lhci assert`;
console.log(`💻 Executing LHCI...`);
try {
const output = execSync(lhciCommand, {
execSync(lhciCommand, {
encoding: 'utf8',
stdio: ['inherit', 'pipe', 'inherit'], // Pipe stdout so we can parse it
stdio: 'inherit',
});
} catch (err: any) {
console.warn('⚠️ LHCI assertion finished with warnings or errors.');
// We continue to show the table even if assertions failed
}
// 3. Summarize Results (Local & Independent)
const manifestPath = path.join(process.cwd(), '.lighthouseci', 'manifest.json');
if (fs.existsSync(manifestPath)) {
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
console.log(`\n📊 PageSpeed Summary (FOSS - Local Report):\n`);
const summaryTable = manifest.map((entry: any) => {
const s = entry.summary;
return {
URL: entry.url.replace(targetUrl, ''),
Perf: Math.round(s.performance * 100),
Acc: Math.round(s.accessibility * 100),
BP: Math.round(s['best-practices'] * 100),
SEO: Math.round(s.seo * 100),
};
});
console.log(output);
console.table(summaryTable);
// Extract report URL from LHCI output
const reportMatch = output.match(
/Sent to (https:\/\/storage\.googleapis\.com\/lighthouse-infrastructure\.appspot\.com\/reports\/[^\s]+)/,
);
if (reportMatch && reportMatch[1]) {
const reportUrl = reportMatch[1];
console.log(`\n📊 Report URL: ${reportUrl}`);
fs.writeFileSync('pagespeed-report-url.txt', reportUrl);
}
} catch (err: any) {
console.error('❌ LHCI execution failed.');
if (err.stdout) console.log(err.stdout);
if (err.stderr) console.error(err.stderr);
throw err;
// Calculate Average
const avg = {
Perf: Math.round(
summaryTable.reduce((acc: any, curr: any) => acc + curr.Perf, 0) / summaryTable.length,
),
Acc: Math.round(
summaryTable.reduce((acc: any, curr: any) => acc + curr.Acc, 0) / summaryTable.length,
),
BP: Math.round(
summaryTable.reduce((acc: any, curr: any) => acc + curr.BP, 0) / summaryTable.length,
),
SEO: Math.round(
summaryTable.reduce((acc: any, curr: any) => acc + curr.SEO, 0) / summaryTable.length,
),
};
console.log(`\n📈 Average Scores:`);
console.log(` Performance: ${avg.Perf > 90 ? '✅' : '⚠️'} ${avg.Perf}`);
console.log(` Accessibility: ${avg.Acc > 90 ? '✅' : '⚠️'} ${avg.Acc}`);
console.log(` Best Practices: ${avg.BP > 90 ? '✅' : '⚠️'} ${avg.BP}`);
console.log(` SEO: ${avg.SEO > 90 ? '✅' : '⚠️'} ${avg.SEO}`);
}
console.log(`\n✨ PageSpeed tests completed successfully!`);