Compare commits
15 Commits
v2.3.27
...
fix/na-cab
| Author | SHA1 | Date | |
|---|---|---|---|
| 76cbdaa37e | |||
| 5b1a2eff7f | |||
| 60422da644 | |||
| fc6da4e9aa | |||
| f7411d3dc4 | |||
| 7be2ec605c | |||
| bd00175912 | |||
| 9c9663eb88 | |||
| 402b5c30c6 | |||
| 2bb0381de4 | |||
| 614b0a5ffd | |||
| b1569a1ce2 | |||
| 2c35c6b8aa | |||
| bff150b1a7 | |||
| 8ffb1e5f6a |
@@ -256,6 +256,10 @@ jobs:
|
||||
# Analytics
|
||||
UMAMI_WEBSITE_ID: ${{ secrets.UMAMI_WEBSITE_ID || vars.UMAMI_WEBSITE_ID }}
|
||||
UMAMI_API_ENDPOINT: ${{ secrets.UMAMI_API_ENDPOINT || vars.UMAMI_API_ENDPOINT || 'https://analytics.infra.mintel.me' }}
|
||||
|
||||
# Notifications
|
||||
GOTIFY_URL: ${{ secrets.GOTIFY_URL }}
|
||||
GOTIFY_TOKEN: ${{ secrets.GOTIFY_TOKEN }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
@@ -309,6 +313,10 @@ jobs:
|
||||
echo "UMAMI_WEBSITE_ID=$UMAMI_WEBSITE_ID"
|
||||
echo "UMAMI_API_ENDPOINT=$UMAMI_API_ENDPOINT"
|
||||
echo ""
|
||||
echo "# Notifications"
|
||||
echo "GOTIFY_URL=$GOTIFY_URL"
|
||||
echo "GOTIFY_TOKEN=$GOTIFY_TOKEN"
|
||||
echo ""
|
||||
echo "TARGET=$TARGET"
|
||||
echo "SENTRY_ENVIRONMENT=$TARGET"
|
||||
echo "PROJECT_NAME=$PROJECT_NAME"
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -28,3 +28,4 @@ backups/
|
||||
|
||||
# Local data / backups
|
||||
.data/
|
||||
scratch/
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ImageResponse } from 'next/og';
|
||||
import { getPageBySlug } from '@/lib/pages';
|
||||
import { OGImageTemplate } from '@/components/OGImageTemplate';
|
||||
import { getOgFonts, OG_IMAGE_SIZE } from '@/lib/og-helper';
|
||||
import { getOgFonts, OG_IMAGE_SIZE, getOgAssets } from '@/lib/og-helper';
|
||||
|
||||
export const size = OG_IMAGE_SIZE;
|
||||
export const contentType = 'image/png';
|
||||
@@ -20,9 +20,12 @@ export default async function Image({
|
||||
}
|
||||
|
||||
const fonts = await getOgFonts();
|
||||
const { logo, bgImage } = getOgAssets();
|
||||
|
||||
return new ImageResponse(
|
||||
<OGImageTemplate
|
||||
image={bgImage}
|
||||
logo={logo}
|
||||
title={pageData.frontmatter.title}
|
||||
description={pageData.frontmatter.excerpt}
|
||||
label="Information"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ImageResponse } from 'next/og';
|
||||
import { getPostBySlug } from '@/lib/blog';
|
||||
import { OGImageTemplate } from '@/components/OGImageTemplate';
|
||||
import { getOgFonts, OG_IMAGE_SIZE } from '@/lib/og-helper';
|
||||
import { getOgFonts, OG_IMAGE_SIZE, getOgAssets } from '@/lib/og-helper';
|
||||
import { SITE_URL } from '@/lib/schema';
|
||||
|
||||
export const size = OG_IMAGE_SIZE;
|
||||
@@ -35,6 +35,7 @@ export default async function Image({
|
||||
}
|
||||
|
||||
const fonts = await getOgFonts();
|
||||
const { logo, bgImage } = getOgAssets();
|
||||
|
||||
// We don't have request.url here, but we can assume the domain from SITE_URL or config
|
||||
// For local images during dev, relative paths in <img> might not work in Satori
|
||||
@@ -55,10 +56,11 @@ export default async function Image({
|
||||
|
||||
return new ImageResponse(
|
||||
<OGImageTemplate
|
||||
logo={logo}
|
||||
title={post.frontmatter.title}
|
||||
description={post.frontmatter.excerpt}
|
||||
label={post.frontmatter.category || 'Blog'}
|
||||
image={base64Image || featuredImage}
|
||||
image={base64Image || featuredImage || bgImage}
|
||||
/>,
|
||||
{
|
||||
...OG_IMAGE_SIZE,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ImageResponse } from 'next/og';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
import { OGImageTemplate } from '@/components/OGImageTemplate';
|
||||
import { getOgFonts, OG_IMAGE_SIZE } from '@/lib/og-helper';
|
||||
import { getOgFonts, OG_IMAGE_SIZE, getOgAssets } from '@/lib/og-helper';
|
||||
|
||||
export const size = OG_IMAGE_SIZE;
|
||||
export const contentType = 'image/png';
|
||||
@@ -11,9 +11,16 @@ export default async function Image({ params }: { params: Promise<{ locale: stri
|
||||
const { locale } = await params;
|
||||
const t = await getTranslations({ locale, namespace: 'Blog.meta' });
|
||||
const fonts = await getOgFonts();
|
||||
const { logo, bgImage } = getOgAssets();
|
||||
|
||||
return new ImageResponse(
|
||||
<OGImageTemplate title={t('title')} description={t('description')} label="Blog" />,
|
||||
<OGImageTemplate
|
||||
image={bgImage}
|
||||
logo={logo}
|
||||
title={t('title')}
|
||||
description={t('description')}
|
||||
label="Blog"
|
||||
/>,
|
||||
{
|
||||
...OG_IMAGE_SIZE,
|
||||
fonts,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ImageResponse } from 'next/og';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
import { OGImageTemplate } from '@/components/OGImageTemplate';
|
||||
import { getOgFonts, OG_IMAGE_SIZE } from '@/lib/og-helper';
|
||||
import { getOgFonts, OG_IMAGE_SIZE, getOgAssets } from '@/lib/og-helper';
|
||||
|
||||
export const size = OG_IMAGE_SIZE;
|
||||
export const contentType = 'image/png';
|
||||
@@ -11,12 +11,19 @@ export default async function Image({ params }: { params: Promise<{ locale: stri
|
||||
const { locale } = await params;
|
||||
const t = await getTranslations({ locale, namespace: 'Contact' });
|
||||
const fonts = await getOgFonts();
|
||||
const { logo, bgImage } = getOgAssets();
|
||||
|
||||
const title = t('meta.title') || t('title');
|
||||
const description = t('meta.description') || t('subtitle');
|
||||
|
||||
return new ImageResponse(
|
||||
<OGImageTemplate title={title} description={description} label="Contact" />,
|
||||
<OGImageTemplate
|
||||
image={bgImage}
|
||||
logo={logo}
|
||||
title={title}
|
||||
description={description}
|
||||
label="Contact"
|
||||
/>,
|
||||
{
|
||||
...OG_IMAGE_SIZE,
|
||||
fonts,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ImageResponse } from 'next/og';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
import { OGImageTemplate } from '@/components/OGImageTemplate';
|
||||
import { getOgFonts, OG_IMAGE_SIZE } from '@/lib/og-helper';
|
||||
import { getOgFonts, OG_IMAGE_SIZE, getOgAssets } from '@/lib/og-helper';
|
||||
|
||||
export const size = OG_IMAGE_SIZE;
|
||||
export const contentType = 'image/png';
|
||||
@@ -11,9 +11,12 @@ export default async function Image({ params }: { params: Promise<{ locale: stri
|
||||
const { locale } = await params;
|
||||
const t = await getTranslations({ locale, namespace: 'Index.meta' });
|
||||
const fonts = await getOgFonts();
|
||||
const { logo, bgImage } = getOgAssets();
|
||||
|
||||
return new ImageResponse(
|
||||
<OGImageTemplate
|
||||
image={bgImage}
|
||||
logo={logo}
|
||||
title={t('title')}
|
||||
description={t('description')}
|
||||
label="Reliable Energy Infrastructure"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ImageResponse } from 'next/og';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
import { OGImageTemplate } from '@/components/OGImageTemplate';
|
||||
import { getOgFonts, OG_IMAGE_SIZE } from '@/lib/og-helper';
|
||||
import { getOgFonts, OG_IMAGE_SIZE, getOgAssets } from '@/lib/og-helper';
|
||||
|
||||
export const size = OG_IMAGE_SIZE;
|
||||
export const contentType = 'image/png';
|
||||
@@ -11,6 +11,7 @@ export default async function Image({ params }: { params: Promise<{ locale: stri
|
||||
const { locale } = await params;
|
||||
const t = await getTranslations({ locale, namespace: 'Products' });
|
||||
const fonts = await getOgFonts();
|
||||
const { logo, bgImage } = getOgAssets();
|
||||
|
||||
const title = t.has('meta.title')
|
||||
? t('meta.title')
|
||||
@@ -20,7 +21,13 @@ export default async function Image({ params }: { params: Promise<{ locale: stri
|
||||
const description = t('meta.description') || t('subtitle');
|
||||
|
||||
return new ImageResponse(
|
||||
<OGImageTemplate title={title} description={description} label="Products" />,
|
||||
<OGImageTemplate
|
||||
image={bgImage}
|
||||
logo={logo}
|
||||
title={title}
|
||||
description={description}
|
||||
label="Products"
|
||||
/>,
|
||||
{
|
||||
...OG_IMAGE_SIZE,
|
||||
fonts,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ImageResponse } from 'next/og';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
import { OGImageTemplate } from '@/components/OGImageTemplate';
|
||||
import { getOgFonts, OG_IMAGE_SIZE } from '@/lib/og-helper';
|
||||
import { getOgFonts, OG_IMAGE_SIZE, getOgAssets } from '@/lib/og-helper';
|
||||
|
||||
export const size = OG_IMAGE_SIZE;
|
||||
export const contentType = 'image/png';
|
||||
@@ -11,12 +11,19 @@ export default async function Image({ params }: { params: Promise<{ locale: stri
|
||||
const { locale } = await params;
|
||||
const t = await getTranslations({ locale, namespace: 'Team' });
|
||||
const fonts = await getOgFonts();
|
||||
const { logo, bgImage } = getOgAssets();
|
||||
|
||||
const title = t('meta.title') || t('hero.subtitle');
|
||||
const description = t('meta.description') || t('hero.title');
|
||||
|
||||
return new ImageResponse(
|
||||
<OGImageTemplate title={title} description={description} label="Our Team" />,
|
||||
<OGImageTemplate
|
||||
image={bgImage}
|
||||
logo={logo}
|
||||
title={title}
|
||||
description={description}
|
||||
label="Our Team"
|
||||
/>,
|
||||
{
|
||||
...OG_IMAGE_SIZE,
|
||||
fonts,
|
||||
|
||||
@@ -61,7 +61,7 @@ export async function sendContactFormAction(formData: FormData) {
|
||||
productName,
|
||||
message,
|
||||
};
|
||||
fs.appendFileSync(backupFile, JSON.stringify(leadData) + '\\n');
|
||||
fs.appendFileSync(backupFile, JSON.stringify(leadData) + '\n');
|
||||
logger.info('Successfully saved lead to local backup file', { backupFile });
|
||||
} catch (backupError) {
|
||||
logger.error('Failed to write to local leads backup', { error: String(backupError) });
|
||||
@@ -156,7 +156,7 @@ export async function sendContactFormAction(formData: FormData) {
|
||||
|
||||
// Notify via Gotify (Internal)
|
||||
await services.notifications.notify({
|
||||
title: `📩 ${notificationSubject}`,
|
||||
title: `📩 [KLZ] ${notificationSubject}`,
|
||||
message: `New message from ${name} (${email}):\n\n${message}`,
|
||||
priority: 5,
|
||||
});
|
||||
|
||||
@@ -24,6 +24,7 @@ import VideoSection from './home/VideoSection';
|
||||
import CTA from './home/CTA';
|
||||
import { AgbHistoryBlock } from './AgbHistoryBlock';
|
||||
import { PDFDownloadBlock } from './PDFDownloadBlock';
|
||||
import ProductTechnicalData from './ProductTechnicalData';
|
||||
function ContactSection(props: any) {
|
||||
return (
|
||||
<div className="p-8 border-2 border-dashed border-primary my-8 text-center text-primary font-bold">
|
||||
@@ -100,6 +101,8 @@ async function Block(props: any) {
|
||||
return <AgbHistoryBlock {...data} />;
|
||||
case 'pdfDownload':
|
||||
return <PDFDownloadBlock {...data} />;
|
||||
case 'productTabs':
|
||||
return <ProductTechnicalData data={data} />;
|
||||
|
||||
default:
|
||||
return (
|
||||
|
||||
@@ -5,6 +5,7 @@ interface OGImageTemplateProps {
|
||||
description?: string;
|
||||
label?: string;
|
||||
image?: string;
|
||||
logo?: string;
|
||||
mode?: 'dark' | 'light' | 'image';
|
||||
}
|
||||
|
||||
@@ -13,6 +14,7 @@ export function OGImageTemplate({
|
||||
description,
|
||||
label,
|
||||
image,
|
||||
logo,
|
||||
mode = 'dark',
|
||||
}: OGImageTemplateProps) {
|
||||
const primaryBlue = '#001a4d';
|
||||
@@ -152,21 +154,26 @@ export function OGImageTemplate({
|
||||
height: '6px',
|
||||
backgroundColor: accentGreen,
|
||||
borderRadius: '3px',
|
||||
marginRight: '24px',
|
||||
marginRight: '32px',
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '24px',
|
||||
fontWeight: 700,
|
||||
color: 'white',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.15em',
|
||||
display: 'flex',
|
||||
}}
|
||||
>
|
||||
KLZ Cables
|
||||
</div>
|
||||
{logo ? (
|
||||
/* eslint-disable-next-line @next/next/no-img-element */
|
||||
<img src={logo} alt="KLZ Cables" height="40" style={{ objectFit: 'contain' }} />
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
fontSize: '24px',
|
||||
fontWeight: 700,
|
||||
color: 'white',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.15em',
|
||||
display: 'flex',
|
||||
}}
|
||||
>
|
||||
KLZ Cables
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Saturated Blue Brand Strip */}
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -65,11 +65,8 @@ export async function getPostBySlug(slug: string, locale: string): Promise<PostD
|
||||
const fileContent = await fs.readFile(filePath, 'utf-8');
|
||||
const { data, content } = matter(fileContent);
|
||||
|
||||
// Fix MDX data props dropped by next-mdx-remote
|
||||
// Payload serializes as data={{"items":...}} which Acorn treats as a Block statement.
|
||||
const fixedContent = content.replace(/data=\{\{([\s\S]*?)\}\}/g, (match, p1) => {
|
||||
return 'data="{' + p1.replace(/"/g, '"') + '}"';
|
||||
});
|
||||
const { fixMdxDataProps } = await import('./mdx-utils');
|
||||
const fixedContent = fixMdxDataProps(content);
|
||||
|
||||
let parsedContent = fixedContent;
|
||||
try {
|
||||
|
||||
68
lib/mdx-utils.ts
Normal file
68
lib/mdx-utils.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
export function fixMdxDataProps(content: string): string {
|
||||
let fixedContent = content;
|
||||
let dataIndex = fixedContent.indexOf('data={{');
|
||||
|
||||
while (dataIndex !== -1) {
|
||||
let openCount = 0;
|
||||
let endIndex = -1;
|
||||
const startObj = dataIndex + 6; // index of the first '{' in 'data={{'
|
||||
|
||||
let inString = false;
|
||||
let isEscaped = false;
|
||||
|
||||
for (let i = startObj; i < fixedContent.length; i++) {
|
||||
const char = fixedContent[i];
|
||||
|
||||
if (isEscaped) {
|
||||
isEscaped = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === '\\') {
|
||||
isEscaped = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === '"') {
|
||||
inString = !inString;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!inString) {
|
||||
if (char === '{') {
|
||||
openCount++;
|
||||
} else if (char === '}') {
|
||||
openCount--;
|
||||
if (openCount === 0) {
|
||||
endIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (endIndex !== -1) {
|
||||
// jsonStr is the content INSIDE the outer curly braces
|
||||
const jsonStr = fixedContent.substring(startObj + 1, endIndex);
|
||||
const safeJsonStr = jsonStr.replace(/"/g, '"');
|
||||
const replacement = `data="{${safeJsonStr}}"`;
|
||||
|
||||
// We also need to consume the closing `}` of the `data={{`
|
||||
const nextCharIndex = endIndex + 1;
|
||||
const skipChars =
|
||||
nextCharIndex < fixedContent.length && fixedContent[nextCharIndex] === '}' ? 2 : 1;
|
||||
|
||||
fixedContent =
|
||||
fixedContent.substring(0, dataIndex) +
|
||||
replacement +
|
||||
fixedContent.substring(endIndex + skipChars);
|
||||
|
||||
dataIndex = fixedContent.indexOf('data={{', dataIndex + replacement.length);
|
||||
} else {
|
||||
// Malformed braces, prevent infinite loop
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return fixedContent;
|
||||
}
|
||||
@@ -55,3 +55,36 @@ export const OG_IMAGE_SIZE = {
|
||||
width: 1200,
|
||||
height: 630,
|
||||
};
|
||||
|
||||
/**
|
||||
* Loads a local image and converts it to a base64 data URI for Satori
|
||||
*/
|
||||
export function getLocalImageBase64(filename: string): string | undefined {
|
||||
try {
|
||||
const filePath = join(process.cwd(), 'public', filename);
|
||||
const buffer = readFileSync(filePath);
|
||||
const ext = filename.split('.').pop()?.toLowerCase();
|
||||
const mimeType =
|
||||
ext === 'png'
|
||||
? 'image/png'
|
||||
: ext === 'jpg' || ext === 'jpeg'
|
||||
? 'image/jpeg'
|
||||
: 'image/svg+xml';
|
||||
return `data:${mimeType};base64,${buffer.toString('base64')}`;
|
||||
} catch (error) {
|
||||
console.error(`[OG] Failed to load local image ${filename}:`, error);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns standard assets for OG images
|
||||
*/
|
||||
export function getOgAssets() {
|
||||
return {
|
||||
logo: getLocalImageBase64('logo-white.png'),
|
||||
bgImage: getLocalImageBase64(
|
||||
'media/aerial-view-of-electricity-station-surrounded-with-2023-11-27-05-33-40-utc-scaled.jpg',
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -53,11 +53,8 @@ export async function getPageBySlug(slug: string, locale: string): Promise<PageD
|
||||
const fileContent = await fs.readFile(filePath, 'utf-8');
|
||||
const { data, content } = matter(fileContent);
|
||||
|
||||
// Fix MDX data props dropped by next-mdx-remote
|
||||
// Payload serializes as data={{"items":...}} which Acorn treats as a Block statement.
|
||||
const fixedContent = content.replace(/data=\{\{([\s\S]*?)\}\}/g, (match, p1) => {
|
||||
return 'data="{' + p1.replace(/"/g, '"') + '}"';
|
||||
});
|
||||
const { fixMdxDataProps } = await import('./mdx-utils');
|
||||
const fixedContent = fixMdxDataProps(content);
|
||||
|
||||
let parsedContent = fixedContent;
|
||||
try {
|
||||
|
||||
@@ -39,11 +39,8 @@ export async function getProductBySlug(slug: string, locale: string): Promise<Pr
|
||||
const fileContent = await fs.readFile(filePath, 'utf-8');
|
||||
const { data, content } = matter(fileContent);
|
||||
|
||||
// Fix MDX data props dropped by next-mdx-remote
|
||||
// Payload serializes as data={{"items":...}} which Acorn treats as a Block statement.
|
||||
const fixedContent = content.replace(/data=\{\{([\s\S]*?)\}\}/g, (match, p1) => {
|
||||
return 'data="{' + p1.replace(/"/g, '"') + '}"';
|
||||
});
|
||||
const { fixMdxDataProps } = await import('./mdx-utils');
|
||||
const fixedContent = fixMdxDataProps(content);
|
||||
|
||||
let parsedContent = fixedContent;
|
||||
try {
|
||||
|
||||
@@ -56,6 +56,11 @@ export class GlitchtipErrorReportingService implements ErrorReportingService {
|
||||
tracesSampleRate: this.options.tracesSampleRate ?? 0.1,
|
||||
replaysOnErrorSampleRate: 1.0,
|
||||
replaysSessionSampleRate: 0.1,
|
||||
ignoreErrors: [
|
||||
'ChunkLoadError',
|
||||
'Failed to fetch dynamically imported module',
|
||||
'Failed to find Server Action',
|
||||
],
|
||||
});
|
||||
}
|
||||
return Sentry;
|
||||
|
||||
@@ -116,7 +116,7 @@
|
||||
"prepare": "husky",
|
||||
"preinstall": "npx only-allow pnpm"
|
||||
},
|
||||
"version": "2.3.27",
|
||||
"version": "2.3.32",
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"@parcel/watcher",
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -12,4 +12,5 @@ Sentry.init({
|
||||
|
||||
// Setting this option to true will print useful information to the console while you're setting up Sentry.
|
||||
debug: false,
|
||||
ignoreErrors: ['failed to pipe response'],
|
||||
});
|
||||
|
||||
@@ -12,4 +12,5 @@ Sentry.init({
|
||||
|
||||
// Setting this option to true will print useful information to the console while you're setting up Sentry.
|
||||
debug: false,
|
||||
ignoreErrors: ['failed to pipe response'],
|
||||
});
|
||||
|
||||
47
tests/glitchtip-error-reporting.test.ts
Normal file
47
tests/glitchtip-error-reporting.test.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { GlitchtipErrorReportingService } from '../lib/services/errors/glitchtip-error-reporting-service';
|
||||
|
||||
// Mock the LoggerService
|
||||
const mockLogger = {
|
||||
child: vi.fn().mockReturnThis(),
|
||||
info: vi.fn(),
|
||||
error: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
} as any;
|
||||
|
||||
const mockSentryInit = vi.fn();
|
||||
|
||||
vi.mock('@sentry/nextjs', () => ({
|
||||
init: (...args: any[]) => mockSentryInit(...args),
|
||||
captureException: vi.fn(),
|
||||
captureMessage: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('GlitchtipErrorReportingService', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should ignore version drift errors (ChunkLoadError and Server Action) in Sentry config', async () => {
|
||||
// Simulate window to force client-side init behavior instantly
|
||||
const originalWindow = global.window;
|
||||
global.window = {
|
||||
requestIdleCallback: (cb: Function) => cb(),
|
||||
} as any;
|
||||
|
||||
const service = new GlitchtipErrorReportingService({ enabled: true }, mockLogger);
|
||||
|
||||
// Give the dynamic import a moment to resolve
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
expect(mockSentryInit).toHaveBeenCalledOnce();
|
||||
const initCallArgs = mockSentryInit.mock.calls[0][0];
|
||||
|
||||
expect(initCallArgs.ignoreErrors).toBeDefined();
|
||||
expect(initCallArgs.ignoreErrors).toContain('ChunkLoadError');
|
||||
expect(initCallArgs.ignoreErrors).toContain('Failed to find Server Action');
|
||||
|
||||
global.window = originalWindow;
|
||||
});
|
||||
});
|
||||
14
tests/mdx-content-mapping.test.tsx
Normal file
14
tests/mdx-content-mapping.test.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
describe('MDXContent Mapping', () => {
|
||||
it('should have productTabs in the switch statement to prevent Unknown Block Type errors', () => {
|
||||
const filePath = path.join(process.cwd(), 'components/MDXContent.tsx');
|
||||
const fileContent = fs.readFileSync(filePath, 'utf-8');
|
||||
|
||||
// Check if the switch case for productTabs exists
|
||||
expect(fileContent).toContain("case 'productTabs':");
|
||||
expect(fileContent).toContain('<ProductTechnicalData');
|
||||
});
|
||||
});
|
||||
22
tests/mdx-regex.test.ts
Normal file
22
tests/mdx-regex.test.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { fixMdxDataProps } from '../lib/mdx-utils';
|
||||
|
||||
describe('MDX Data Props Fixer', () => {
|
||||
it('should correctly parse MDX with nested JSON containing }}', () => {
|
||||
// This string simulates the exact structure that causes the bug in n2x2y.mdx
|
||||
const mdxInput = `<Block type="productTabs" data={{"content":{"root":{"children":[]}},"id":"123"}} />`;
|
||||
|
||||
const result = fixMdxDataProps(mdxInput);
|
||||
|
||||
const expected = `<Block type="productTabs" data="{"content":{"root":{"children":[]}},"id":"123"}" />`;
|
||||
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
|
||||
it('should not prematurely truncate if } appears inside a string literal', () => {
|
||||
const mdxInput = `<Block type="productTabs" data={{"content":{"text":"}>"}}} />`;
|
||||
const result = fixMdxDataProps(mdxInput);
|
||||
const expected = `<Block type="productTabs" data="{"content":{"text":"}>"}}" />`;
|
||||
expect(result).toBe(expected.replace('>', '>'));
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user