From c0938ca7164df71daeb702b77b7315bc2db39c81 Mon Sep 17 00:00:00 2001 From: Ellie Re'em Date: Mon, 9 Jun 2025 15:35:56 +0100 Subject: [PATCH 1/5] fix: ensure server only env variables aren't prefixed with next_public --- .env.example | 14 +++++++------- app/api/feedback/route.ts | 6 +++--- app/api/follow-up-questions/route.ts | 6 +++--- app/api/quality-check-letter/route.ts | 6 +++--- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.env.example b/.env.example index 6d2966a..85c513c 100644 --- a/.env.example +++ b/.env.example @@ -17,7 +17,7 @@ NEXT_PUBLIC_ENV=development NEXT_PUBLIC_GOOGLE_SITE_VERIFICATION= # Typeform Feedback URL (optional) -NEXT_PUBLIC_TYPEFORM_FEEDBACK_URL= +TYPEFORM_FEEDBACK_URL= # Google AI API Key for Gemini GOOGLE_AI_API_KEY= @@ -25,11 +25,11 @@ GOOGLE_AI_API_KEY= # Slack Webhook URL for notifications SLACK_WEBHOOK_URL= -# Zapier Webhook URL for feedback collection -NEXT_PUBLIC_ZAPIER_FEEDBACK_WEBHOOK_URL= -NEXT_PUBLIC_ZAPIER_FOLLOWUP_WEBHOOK_URL= -NEXT_PUBLIC_ZAPIER_QUALITY_WEBHOOK_URL= +# Zapier Webhook URLs for data collection +ZAPIER_FEEDBACK_WEBHOOK_URL= +ZAPIER_FOLLOWUP_WEBHOOK_URL= +ZAPIER_QUALITY_WEBHOOK_URL= +ZAPIER_DEVELOPMENT_INPUT_COLLECTION_WEBHOOK_URL= # Zapier Webhook Token for authentication -ZAPIER_WEBHOOK_TOKEN= - +ZAPIER_WEBHOOK_TOKEN= \ No newline at end of file diff --git a/app/api/feedback/route.ts b/app/api/feedback/route.ts index 9a5b474..89bef9c 100644 --- a/app/api/feedback/route.ts +++ b/app/api/feedback/route.ts @@ -5,12 +5,12 @@ import { NextResponse } from 'next/server'; export async function POST(request: Request) { rollbar.info('feedback: Received feedback submission'); try { - if (!process.env.NEXT_PUBLIC_ZAPIER_FEEDBACK_WEBHOOK_URL) { + if (!process.env.ZAPIER_FEEDBACK_WEBHOOK_URL) { return NextResponse.json({ error: 'Zapier webhook URL not configured' }, { status: 500 }); } const body = await request.json(); - await sendToZapier(process.env.NEXT_PUBLIC_ZAPIER_FEEDBACK_WEBHOOK_URL, body); + await sendToZapier(process.env.ZAPIER_FEEDBACK_WEBHOOK_URL, body); return NextResponse.json({ success: true }); } catch (error: any) { @@ -33,4 +33,4 @@ export async function OPTIONS() { 'Access-Control-Allow-Headers': 'Content-Type', }, }); -} \ No newline at end of file +} diff --git a/app/api/follow-up-questions/route.ts b/app/api/follow-up-questions/route.ts index fcee799..0028ee7 100644 --- a/app/api/follow-up-questions/route.ts +++ b/app/api/follow-up-questions/route.ts @@ -46,7 +46,7 @@ export async function POST(request: Request) { }); // Send questions to Zapier if webhook URL is configured - if (process.env.NEXT_PUBLIC_ZAPIER_FOLLOWUP_WEBHOOK_URL) { + if (process.env.ZAPIER_FOLLOWUP_WEBHOOK_URL) { const zapierPayload = { date: new Date().toISOString(), Q1: questions[0]?.question || '', @@ -57,7 +57,7 @@ export async function POST(request: Request) { Q6: questions[5]?.question || '', }; - await sendToZapier(process.env.NEXT_PUBLIC_ZAPIER_FOLLOWUP_WEBHOOK_URL, zapierPayload); + await sendToZapier(process.env.ZAPIER_FOLLOWUP_WEBHOOK_URL, zapierPayload); } return NextResponse.json(questions); @@ -73,4 +73,4 @@ export async function POST(request: Request) { return NextResponse.json({ error: errorMessage }, { status }); } -} \ No newline at end of file +} diff --git a/app/api/quality-check-letter/route.ts b/app/api/quality-check-letter/route.ts index 2af6017..922503b 100644 --- a/app/api/quality-check-letter/route.ts +++ b/app/api/quality-check-letter/route.ts @@ -51,8 +51,8 @@ export async function POST(request: Request) { const qualityCheckResult = await retryWithDelay(generateQualityCheck); // Send critical issues to Zapier if webhook URL is configured - // if (qualityCheckResult.severity === 'critical' && process.env.NEXT_PUBLIC_ZAPIER_QUALITY_WEBHOOK_URL) { - if (process.env.NEXT_PUBLIC_ZAPIER_QUALITY_WEBHOOK_URL) { + // if (qualityCheckResult.severity === 'critical' && process.env.ZAPIER_QUALITY_WEBHOOK_URL) { + if (process.env.ZAPIER_QUALITY_WEBHOOK_URL) { const zapierPayload = { date: new Date().toISOString(), issue1: qualityCheckResult.issues[0] @@ -75,7 +75,7 @@ export async function POST(request: Request) { : '', }; - await sendToZapier(process.env.NEXT_PUBLIC_ZAPIER_QUALITY_WEBHOOK_URL, zapierPayload); + await sendToZapier(process.env.ZAPIER_QUALITY_WEBHOOK_URL, zapierPayload); } rollbar.info('QualityCheckLetter: Successfully parsed quality check result', { From 9f86600dcd0fa82dfc67b83415c4ac7bdde1e06f Mon Sep 17 00:00:00 2001 From: Ellie Re'em Date: Mon, 9 Jun 2025 15:38:54 +0100 Subject: [PATCH 2/5] feat: add development mode data collection --- app/api/dev-data-collection/route.ts | 112 +++++++++++++++++ app/client-layout.tsx | 10 +- .../components/letter-review.tsx | 117 ++++++++++++++++-- components/dev/development-warning.tsx | 114 +++++++++++++++++ components/feedback/disclaimer-banner.tsx | 2 +- lib/dev/data-collection.ts | 55 ++++++++ 6 files changed, 397 insertions(+), 13 deletions(-) create mode 100644 app/api/dev-data-collection/route.ts create mode 100644 components/dev/development-warning.tsx create mode 100644 lib/dev/data-collection.ts diff --git a/app/api/dev-data-collection/route.ts b/app/api/dev-data-collection/route.ts new file mode 100644 index 0000000..1c23512 --- /dev/null +++ b/app/api/dev-data-collection/route.ts @@ -0,0 +1,112 @@ +import { handleApiError, serverInstance as rollbar } from '@/lib/rollbar'; +import { sendToZapier } from '@/lib/zapier'; +import { NextResponse } from 'next/server'; + +export async function POST(request: Request) { + rollbar.info('dev-data-collection: Received development data submission'); + + try { + // Only allow in development environment + if (process.env.NEXT_PUBLIC_ENV !== 'development') { + rollbar.warning('dev-data-collection: Attempted to use in non-development environment'); + return NextResponse.json( + { error: 'This endpoint is only available in development' }, + { status: 403 }, + ); + } + + if (!process.env.ZAPIER_DEVELOPMENT_INPUT_COLLECTION_WEBHOOK_URL) { + rollbar.warning('dev-data-collection: Zapier development webhook URL not configured'); + return NextResponse.json( + { error: 'Development webhook URL not configured' }, + { status: 500 }, + ); + } + + const body = await request.json(); + + // Validate required fields + if (!body.formData || !body.generatedLetter) { + rollbar.error('dev-data-collection: Missing required fields', { body }); + return NextResponse.json( + { error: 'Missing required fields: formData and generatedLetter' }, + { status: 400 }, + ); + } + + // Prepare the payload for Zapier + const zapierPayload = { + timestamp: new Date().toISOString(), + environment: 'development', + + // Platform Information + platform_id: body.formData.platformInfo?.platformId || '', + platform_name: body.formData.platformInfo?.platformName || '', + platform_is_custom: body.formData.platformInfo?.isCustom || false, + platform_custom_name: body.formData.platformInfo?.customName || '', + + // Reporting Information + reporting_status: body.formData.reportingInfo?.status || '', + + // Initial Questions + content_type: body.formData.initialQuestions?.contentType || '', + content_context: body.formData.initialQuestions?.contentContext || '', + content_location_type: body.formData.initialQuestions?.contentLocationType || '', + content_url: body.formData.initialQuestions?.contentUrl || '', + content_description: body.formData.initialQuestions?.contentDescription || '', + image_upload_date: body.formData.initialQuestions?.imageUploadDate || '', + image_taken_date: body.formData.initialQuestions?.imageTakenDate || '', + ownership_evidence: body.formData.initialQuestions?.ownershipEvidence || '', + impact_statement: body.formData.initialQuestions?.impactStatement || '', + + // Reporting Details + standard_process_details: body.formData.reportingDetails?.standardProcessDetails || '', + escalated_process_details: body.formData.reportingDetails?.escalatedProcessDetails || '', + response_received: body.formData.reportingDetails?.responseReceived || '', + additional_steps_taken: body.formData.reportingDetails?.additionalStepsTaken || '', + + // Follow-up Data + follow_up_questions_count: body.formData.followUpData?.questions?.length || 0, + follow_up_answers: JSON.stringify(body.formData.followUpData?.answers || {}), + + // Generated Letter + letter_subject: body.generatedLetter?.subject || '', + letter_body: body.generatedLetter?.body || '', + letter_next_steps: JSON.stringify(body.generatedLetter?.nextSteps || []), + + // Additional metadata + session_id: body.sessionId || '', + user_agent: request.headers.get('user-agent') || '', + completion_time_seconds: body.completionTimeSeconds || 0, + }; + + await sendToZapier(process.env.ZAPIER_DEVELOPMENT_INPUT_COLLECTION_WEBHOOK_URL, zapierPayload); + + rollbar.info('dev-data-collection: Successfully sent development data to Zapier', { + platform: zapierPayload.platform_name, + contentType: zapierPayload.content_type, + hasFollowUp: zapierPayload.follow_up_questions_count > 0, + }); + + return NextResponse.json({ success: true }); + } catch (error: any) { + const { error: errorMessage, status } = handleApiError(error, '/api/dev-data-collection'); + rollbar.error('dev-data-collection: Error processing development data submission', { + error: errorMessage, + status, + stack: error.stack, + }); + return NextResponse.json({ error: errorMessage }, { status }); + } +} + +export async function OPTIONS() { + return new NextResponse(null, { + status: 200, + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'POST, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type', + }, + }); +} diff --git a/app/client-layout.tsx b/app/client-layout.tsx index 25ffc6e..5c068c3 100644 --- a/app/client-layout.tsx +++ b/app/client-layout.tsx @@ -1,5 +1,6 @@ -"use client"; +'use client'; +import { DevelopmentWarning } from '@/components/dev/development-warning'; import { DisclaimerBanner } from '@/components/feedback/disclaimer-banner'; import { Footer } from '@/components/layout/footer'; import { Header } from '@/components/layout/header'; @@ -10,6 +11,7 @@ import { ReactNode } from 'react'; export function ClientLayout({ children }: { children: ReactNode }) { return ( +
@@ -18,9 +20,7 @@ export function ClientLayout({ children }: { children: ReactNode }) {