Skip to content

Commit 605c479

Browse files
committed
Add feedback component.
Signed-off-by: bgravenorst <byron.gravenorst@consensys.net>
1 parent d3609a6 commit 605c479

7 files changed

Lines changed: 2085 additions & 2 deletions

File tree

api/docs-feedback.ts

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import type { VercelRequest, VercelResponse } from '@vercel/node'
2+
import { sheets } from '@googleapis/sheets'
3+
import { GoogleAuth } from 'google-auth-library'
4+
5+
interface FeedbackBody {
6+
page_url: string
7+
rating: 'yes' | 'no'
8+
reason?: string
9+
timestamp?: string
10+
}
11+
12+
function stripHtml(text: string): string {
13+
let prev = text
14+
while (true) {
15+
const next = prev.replace(/<[^>]*>/g, '')
16+
if (next === prev) return next
17+
prev = next
18+
}
19+
}
20+
21+
function sanitize(text: string): string {
22+
return stripHtml(text)
23+
.replace(/!\[([^\]]*)\]\([^)]*\)/g, '$1')
24+
.replace(/\[([^\]]*)\]\([^)]*\)/g, '$1')
25+
.replace(/@/g, '@\u200B')
26+
.replace(/#(\d)/g, '#\u200B$1')
27+
.slice(0, 1000)
28+
.trim()
29+
}
30+
31+
function isValidPageUrl(url: string): boolean {
32+
if (url.startsWith('/')) return /^\/[\w\-./]*$/.test(url)
33+
try {
34+
const parsed = new URL(url)
35+
return parsed.origin === 'https://docs.metamask.io'
36+
} catch {
37+
return false
38+
}
39+
}
40+
41+
function getDeviceType(ua: string): 'mobile' | 'desktop' {
42+
return /Mobile|Android|iPhone|iPad/i.test(ua) ? 'mobile' : 'desktop'
43+
}
44+
45+
const credentials = JSON.parse(
46+
Buffer.from(process.env.GOOGLE_SHEETS_CREDENTIALS!, 'base64').toString()
47+
)
48+
49+
const auth = new GoogleAuth({
50+
credentials,
51+
scopes: ['https://www.googleapis.com/auth/spreadsheets'],
52+
})
53+
54+
const sheetsClient = sheets({ version: 'v4', auth })
55+
56+
async function appendToSheet(row: string[]) {
57+
await sheetsClient.spreadsheets.values.append({
58+
spreadsheetId: process.env.GOOGLE_SHEET_ID!,
59+
range: 'Sheet1!A:E',
60+
valueInputOption: 'RAW',
61+
requestBody: { values: [row] },
62+
})
63+
}
64+
65+
export default async function handler(req: VercelRequest, res: VercelResponse) {
66+
if (req.method !== 'POST') {
67+
return res.status(405).json({ error: 'Method not allowed' })
68+
}
69+
70+
const { page_url: pageUrl, rating, reason, timestamp } = req.body as FeedbackBody
71+
72+
if (!pageUrl || !rating || !['yes', 'no'].includes(rating)) {
73+
return res.status(400).json({ error: 'page_url and rating (yes/no) are required' })
74+
}
75+
76+
if (!isValidPageUrl(pageUrl)) {
77+
return res.status(400).json({ error: 'invalid page_url' })
78+
}
79+
80+
const cleanReason = reason ? sanitize(reason) : ''
81+
82+
if (rating === 'no' && !cleanReason) {
83+
return res.status(400).json({ error: 'reason is required for negative feedback' })
84+
}
85+
86+
const ts = timestamp ?? new Date().toISOString()
87+
88+
try {
89+
await appendToSheet([
90+
ts,
91+
pageUrl,
92+
rating,
93+
cleanReason,
94+
getDeviceType((req.headers['user-agent'] as string) ?? ''),
95+
])
96+
} catch (err) {
97+
console.error('Google Sheets append failed:', err)
98+
}
99+
100+
return res.status(200).json({ ok: true })
101+
}

docusaurus.config.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,18 @@ const config = {
312312
},
313313
],
314314
'./src/plugins/plugin-json-rpc.ts',
315+
function excludeApiPlugin() {
316+
return {
317+
name: 'exclude-api-directory',
318+
configureWebpack() {
319+
return {
320+
module: {
321+
rules: [{ test: /api\/.*\.ts$/, use: 'null-loader' }],
322+
},
323+
}
324+
},
325+
}
326+
},
315327
// Custom Segment plugin for controlled analytics
316328
'./src/plugins/segment',
317329
'./src/plugins/launchdarkly',

0 commit comments

Comments
 (0)