From c21a23fc36a54c1cf77c4b5828bf5bd433d75bf5 Mon Sep 17 00:00:00 2001 From: JP Hwang Date: Fri, 14 Nov 2025 16:13:42 +0000 Subject: [PATCH] Update Netlify function paths and add geolocation and feedback submission functions --- .gitignore | 1 - netlify.toml | 6 +- .../functions/geolocation.js | 0 netlify/functions/submit-feedback.js | 193 ++++++++++++++++++ src/components/PageRatingWidget/index.js | 2 +- 5 files changed, 197 insertions(+), 5 deletions(-) rename {.netlify => netlify}/functions/geolocation.js (100%) create mode 100644 netlify/functions/submit-feedback.js diff --git a/.gitignore b/.gitignore index a11c538a0..81352a416 100644 --- a/.gitignore +++ b/.gitignore @@ -54,7 +54,6 @@ __pycache__ # Local Netlify folder .netlify/* -!.netlify/functions # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/netlify.toml b/netlify.toml index 2c480e08b..e77e01589 100644 --- a/netlify.toml +++ b/netlify.toml @@ -6,9 +6,9 @@ access-control-allow-origin = "*" [functions] - directory = ".netlify/functions/" + directory = "netlify/functions/" -## Remove /current from the path +## Remove /current from the path [[redirects]] from = "/weaviate/current/*" to = "/weaviate/:splat" @@ -853,7 +853,7 @@ from = "/academy/*" to = "https://academy.weaviate.io/" status = 301 -## AWS production guides +## AWS production guides [[redirects]] from = "/deploy/aws" diff --git a/.netlify/functions/geolocation.js b/netlify/functions/geolocation.js similarity index 100% rename from .netlify/functions/geolocation.js rename to netlify/functions/geolocation.js diff --git a/netlify/functions/submit-feedback.js b/netlify/functions/submit-feedback.js new file mode 100644 index 000000000..ffc7c81ec --- /dev/null +++ b/netlify/functions/submit-feedback.js @@ -0,0 +1,193 @@ +exports.handler = async (event) => { + const allowedOriginPattern = process.env.ALLOWED_ORIGIN || '*'; + const requestOrigin = event.headers.origin; + + const isAllowed = () => { + if (allowedOriginPattern === '*') { + return true; + } + if (!requestOrigin) { + return false; + } + if (requestOrigin === allowedOriginPattern) { + return true; + } + if (allowedOriginPattern.startsWith('*.')) { + const baseDomain = allowedOriginPattern.substring(1); + const requestHostname = new URL(requestOrigin).hostname; + return requestHostname.endsWith(baseDomain); + } + return false; + }; + + if (!isAllowed()) { + return { + statusCode: 403, + body: JSON.stringify({ error: 'Origin not allowed' }), + headers: { + 'Access-Control-Allow-Origin': allowedOriginPattern === '*' + ? '*' + : allowedOriginPattern, + }, + }; + } + + const accessControlOrigin = allowedOriginPattern.startsWith('*.') + ? requestOrigin + : allowedOriginPattern; + + const headers = { + 'Access-Control-Allow-Origin': accessControlOrigin, + 'Access-Control-Allow-Headers': 'Content-Type', + 'Access-Control-Allow-Methods': 'POST, OPTIONS', + }; + + // Handle preflight CORS request for browser compatibility + if (event.httpMethod === 'OPTIONS') { + return { + statusCode: 204, + headers, + body: '', + }; + } + + // We only want to handle POST requests + if (event.httpMethod !== 'POST') { + return { + statusCode: 405, + body: 'Method Not Allowed', + headers, + }; + } + + try { + const data = JSON.parse(event.body); + const { WEAVIATE_DOCFEEDBACK_URL, WEAVIATE_DOCFEEDBACK_API_KEY } = + process.env; + + // Basic server-side validation + if (!data.page || typeof data.isPositive !== 'boolean') { + return { + statusCode: 400, + body: JSON.stringify({ + error: 'Missing required fields: page and isPositive.', + }), + headers, + }; + } + + if (!WEAVIATE_DOCFEEDBACK_URL || !WEAVIATE_DOCFEEDBACK_API_KEY) { + // const relevantKeys = Object.keys(process.env).filter( + // k => k.includes('WEAVIATE') || k.includes('FEEDBACK') || k === 'CONTEXT' || k === 'ALLOWED_ORIGIN' || k.startsWith('DEPLOY_') + // ); + console.error('Missing Weaviate environment variables.', { + hasUrl: !!WEAVIATE_DOCFEEDBACK_URL, + hasKey: !!WEAVIATE_DOCFEEDBACK_API_KEY, + context: process.env.CONTEXT, + weaviateRelatedKeyNames: relevantKeys, + weaviateRelatedKeyCount: relevantKeys.length, + }); + return { + statusCode: 500, + body: JSON.stringify({ + error: 'Server configuration error.', + // debug: { + // hasUrl: !!WEAVIATE_DOCFEEDBACK_URL, + // hasKey: !!WEAVIATE_DOCFEEDBACK_API_KEY, + // context: process.env.CONTEXT, + // availableWeaviateVars: relevantKeys, + // availableWeaviateVarCount: relevantKeys.length, + // siteId: process.env.SITE_ID, + // siteName: process.env.SITE_NAME, + // timestamp: new Date().toISOString(), + // }, + }), + headers, + }; + } + + const weaviatePayload = { + class: 'DocFeedback', + properties: { + page: data.page, + isPositive: data.isPositive, + // The frontend sends an array of option indexes as integers + options: data.options || [], + // The frontend can send 'comment' (singular). + comments: data.comment, + timestamp: new Date().toISOString(), + testData: data.testData, // Add the testData flag + hostname: data.hostname, // Add the hostname + }, + }; + + const weaviateUrl = `${WEAVIATE_DOCFEEDBACK_URL}/v1/objects`; + + const response = await fetch(weaviateUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${WEAVIATE_DOCFEEDBACK_API_KEY}`, + }, + body: JSON.stringify(weaviatePayload), + }); + + if (!response.ok) { + let errorBody; + try { + errorBody = await response.json(); + } catch (jsonError) { + // If response is not JSON, get it as text + try { + errorBody = await response.text(); + } catch (textError) { + errorBody = 'Unable to parse error response'; + } + } + console.error('Failed to send data to Weaviate:', { + status: response.status, + statusText: response.statusText, + body: errorBody, + }); + return { + statusCode: response.status, + body: JSON.stringify({ + error: 'Failed to store feedback.', + debug: { + weaviateStatus: response.status, + weaviateStatusText: response.statusText, + weaviateUrl: WEAVIATE_DOCFEEDBACK_URL, + // TODO: Remove errorBody from production after debugging + weaviateError: errorBody, + timestamp: new Date().toISOString(), + context: process.env.CONTEXT, + }, + }), + headers, + }; + } + + return { + statusCode: 200, + body: JSON.stringify({ message: 'Feedback received and stored.' }), + headers, + }; + } catch (error) { + console.error('Error processing feedback:', error); + return { + statusCode: 500, + body: JSON.stringify({ + error: 'There was an error processing your feedback.', + debug: { + errorType: error.name, + errorMessage: error.message, + timestamp: new Date().toISOString(), + // TODO: Remove stack trace from production after debugging + stack: error.stack, + context: process.env.CONTEXT, + }, + }), + headers, + }; + } +}; diff --git a/src/components/PageRatingWidget/index.js b/src/components/PageRatingWidget/index.js index f8e00303f..36e6b95f2 100644 --- a/src/components/PageRatingWidget/index.js +++ b/src/components/PageRatingWidget/index.js @@ -34,7 +34,7 @@ export default function PageRatingWidget() { } try { - const response = await fetch('/.netlify/functions/submit-feedback', { + const response = await fetch('/netlify/functions/submit-feedback', { method: 'POST', headers: { 'Content-Type': 'application/json',