Compare commits
4 Commits
v2.3.29
...
feat/dynam
| Author | SHA1 | Date | |
|---|---|---|---|
| 60422da644 | |||
| fc6da4e9aa | |||
| f7411d3dc4 | |||
| 7be2ec605c |
@@ -32,24 +32,18 @@ export async function sendContactFormAction(formData: FormData) {
|
|||||||
const productName = formData.get('productName') as string | null;
|
const productName = formData.get('productName') as string | null;
|
||||||
|
|
||||||
if (!name || !email || !message) {
|
if (!name || !email || !message) {
|
||||||
logger.warn(
|
logger.warn('Missing required fields in contact form', {
|
||||||
{
|
name: !!name,
|
||||||
name: !!name,
|
email: !!email,
|
||||||
email: !!email,
|
message: !!message,
|
||||||
message: !!message,
|
});
|
||||||
},
|
|
||||||
'Missing required fields in contact form',
|
|
||||||
);
|
|
||||||
return { success: false, error: 'Missing required fields' };
|
return { success: false, error: 'Missing required fields' };
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(
|
logger.info('Payload CMS saving skipped because it has been removed', {
|
||||||
{
|
type: productName ? 'product_quote' : 'contact',
|
||||||
type: productName ? 'product_quote' : 'contact',
|
email,
|
||||||
email,
|
});
|
||||||
},
|
|
||||||
'Payload CMS saving skipped because it has been removed',
|
|
||||||
);
|
|
||||||
|
|
||||||
// 1.5. Simple Fail-Safe Backup to Disk
|
// 1.5. Simple Fail-Safe Backup to Disk
|
||||||
// To ensure leads are never lost if email fails or Gotify is down, we append them to a local JSON Lines file.
|
// To ensure leads are never lost if email fails or Gotify is down, we append them to a local JSON Lines file.
|
||||||
@@ -68,13 +62,13 @@ export async function sendContactFormAction(formData: FormData) {
|
|||||||
message,
|
message,
|
||||||
};
|
};
|
||||||
fs.appendFileSync(backupFile, JSON.stringify(leadData) + '\n');
|
fs.appendFileSync(backupFile, JSON.stringify(leadData) + '\n');
|
||||||
logger.info({ backupFile }, 'Successfully saved lead to local backup file');
|
logger.info('Successfully saved lead to local backup file', { backupFile });
|
||||||
} catch (backupError) {
|
} catch (backupError) {
|
||||||
logger.error({ error: String(backupError) }, 'Failed to write to local leads backup');
|
logger.error('Failed to write to local leads backup', { error: String(backupError) });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Send Emails
|
// 2. Send Emails
|
||||||
logger.info({ email, productName }, 'Sending branded emails');
|
logger.info('Sending branded emails', { email, productName });
|
||||||
|
|
||||||
const notificationSubject = productName
|
const notificationSubject = productName
|
||||||
? `Product Inquiry: ${productName}`
|
? `Product Inquiry: ${productName}`
|
||||||
@@ -94,7 +88,7 @@ export async function sendContactFormAction(formData: FormData) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!isTestSubmission) {
|
if (!isTestSubmission) {
|
||||||
logger.info({ recipients: env.MAIL_RECIPIENTS }, 'Sending internal notification');
|
logger.info('Sending internal notification', { recipients: env.MAIL_RECIPIENTS });
|
||||||
const notificationResult = await sendEmail({
|
const notificationResult = await sendEmail({
|
||||||
replyTo: email,
|
replyTo: email,
|
||||||
subject: notificationSubject,
|
subject: notificationSubject,
|
||||||
@@ -102,21 +96,15 @@ export async function sendContactFormAction(formData: FormData) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (notificationResult.success) {
|
if (notificationResult.success) {
|
||||||
logger.info(
|
logger.info('Notification email sent successfully', {
|
||||||
{
|
messageId: notificationResult.messageId,
|
||||||
messageId: notificationResult.messageId,
|
});
|
||||||
},
|
|
||||||
'Notification email sent successfully',
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
logger.error(
|
logger.error('Notification email DELIVERY FAILED', {
|
||||||
{
|
error: notificationResult.error,
|
||||||
error: notificationResult.error,
|
subject: notificationSubject,
|
||||||
subject: notificationSubject,
|
recipients: env.MAIL_RECIPIENTS,
|
||||||
recipients: env.MAIL_RECIPIENTS,
|
});
|
||||||
},
|
|
||||||
'Notification email DELIVERY FAILED',
|
|
||||||
);
|
|
||||||
services.errors.captureException(
|
services.errors.captureException(
|
||||||
new Error(`Notification email failed: ${notificationResult.error}`),
|
new Error(`Notification email failed: ${notificationResult.error}`),
|
||||||
{
|
{
|
||||||
@@ -127,7 +115,7 @@ export async function sendContactFormAction(formData: FormData) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
logger.info({ email }, 'Skipping notification email for test submission');
|
logger.info('Skipping notification email for test submission', { email });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2b. Send confirmation to Customer (branded as KLZ Cables)
|
// 2b. Send confirmation to Customer (branded as KLZ Cables)
|
||||||
@@ -140,7 +128,7 @@ export async function sendContactFormAction(formData: FormData) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!isTestSubmission) {
|
if (!isTestSubmission) {
|
||||||
logger.info({ to: email }, 'Sending customer confirmation');
|
logger.info('Sending customer confirmation', { to: email });
|
||||||
const confirmationResult = await sendEmail({
|
const confirmationResult = await sendEmail({
|
||||||
to: email,
|
to: email,
|
||||||
subject: confirmationSubject,
|
subject: confirmationSubject,
|
||||||
@@ -148,28 +136,22 @@ export async function sendContactFormAction(formData: FormData) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (confirmationResult.success) {
|
if (confirmationResult.success) {
|
||||||
logger.info(
|
logger.info('Confirmation email sent successfully', {
|
||||||
{
|
messageId: confirmationResult.messageId,
|
||||||
messageId: confirmationResult.messageId,
|
});
|
||||||
},
|
|
||||||
'Confirmation email sent successfully',
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
logger.error(
|
logger.error('Confirmation email DELIVERY FAILED', {
|
||||||
{
|
error: confirmationResult.error,
|
||||||
error: confirmationResult.error,
|
subject: confirmationSubject,
|
||||||
subject: confirmationSubject,
|
to: email,
|
||||||
to: email,
|
});
|
||||||
},
|
|
||||||
'Confirmation email DELIVERY FAILED',
|
|
||||||
);
|
|
||||||
services.errors.captureException(
|
services.errors.captureException(
|
||||||
new Error(`Confirmation email failed: ${confirmationResult.error}`),
|
new Error(`Confirmation email failed: ${confirmationResult.error}`),
|
||||||
{ action: 'sendContactFormAction_confirmation', email },
|
{ action: 'sendContactFormAction_confirmation', email },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
logger.info({ email }, 'Skipping confirmation email for test submission');
|
logger.info('Skipping confirmation email for test submission', { email });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notify via Gotify (Internal)
|
// Notify via Gotify (Internal)
|
||||||
@@ -187,13 +169,10 @@ export async function sendContactFormAction(formData: FormData) {
|
|||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMsg = error instanceof Error ? error.message : String(error);
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
||||||
logger.error(
|
logger.error('Failed to send branded emails', {
|
||||||
{
|
error: errorMsg,
|
||||||
error: errorMsg,
|
stack: error instanceof Error ? error.stack : undefined,
|
||||||
stack: error instanceof Error ? error.stack : undefined,
|
});
|
||||||
},
|
|
||||||
'Failed to send branded emails',
|
|
||||||
);
|
|
||||||
|
|
||||||
services.errors.captureException(error, { action: 'sendContactFormAction', email });
|
services.errors.captureException(error, { action: 'sendContactFormAction', email });
|
||||||
|
|
||||||
|
|||||||
@@ -7,14 +7,36 @@ export function fixMdxDataProps(content: string): string {
|
|||||||
let endIndex = -1;
|
let endIndex = -1;
|
||||||
const startObj = dataIndex + 6; // index of the first '{' in 'data={{'
|
const startObj = dataIndex + 6; // index of the first '{' in 'data={{'
|
||||||
|
|
||||||
|
let inString = false;
|
||||||
|
let isEscaped = false;
|
||||||
|
|
||||||
for (let i = startObj; i < fixedContent.length; i++) {
|
for (let i = startObj; i < fixedContent.length; i++) {
|
||||||
if (fixedContent[i] === '{') {
|
const char = fixedContent[i];
|
||||||
openCount++;
|
|
||||||
} else if (fixedContent[i] === '}') {
|
if (isEscaped) {
|
||||||
openCount--;
|
isEscaped = false;
|
||||||
if (openCount === 0) {
|
continue;
|
||||||
endIndex = i;
|
}
|
||||||
break;
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,6 +56,11 @@ export class GlitchtipErrorReportingService implements ErrorReportingService {
|
|||||||
tracesSampleRate: this.options.tracesSampleRate ?? 0.1,
|
tracesSampleRate: this.options.tracesSampleRate ?? 0.1,
|
||||||
replaysOnErrorSampleRate: 1.0,
|
replaysOnErrorSampleRate: 1.0,
|
||||||
replaysSessionSampleRate: 0.1,
|
replaysSessionSampleRate: 0.1,
|
||||||
|
ignoreErrors: [
|
||||||
|
'ChunkLoadError',
|
||||||
|
'Failed to fetch dynamically imported module',
|
||||||
|
'Failed to find Server Action',
|
||||||
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return Sentry;
|
return Sentry;
|
||||||
|
|||||||
@@ -116,7 +116,7 @@
|
|||||||
"prepare": "husky",
|
"prepare": "husky",
|
||||||
"preinstall": "npx only-allow pnpm"
|
"preinstall": "npx only-allow pnpm"
|
||||||
},
|
},
|
||||||
"version": "2.3.29",
|
"version": "2.3.32",
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"onlyBuiltDependencies": [
|
"onlyBuiltDependencies": [
|
||||||
"@parcel/watcher",
|
"@parcel/watcher",
|
||||||
|
|||||||
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;
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -8,10 +8,15 @@ describe('MDX Data Props Fixer', () => {
|
|||||||
|
|
||||||
const result = fixMdxDataProps(mdxInput);
|
const result = fixMdxDataProps(mdxInput);
|
||||||
|
|
||||||
// The expected output should have the entire JSON object enclosed in data="{...}"
|
|
||||||
// and NO trailing characters left over from the regex truncating early.
|
|
||||||
const expected = `<Block type="productTabs" data="{"content":{"root":{"children":[]}},"id":"123"}" />`;
|
const expected = `<Block type="productTabs" data="{"content":{"root":{"children":[]}},"id":"123"}" />`;
|
||||||
|
|
||||||
expect(result).toBe(expected);
|
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