-
-
Notifications
You must be signed in to change notification settings - Fork 2k
Expand file tree
/
Copy pathdocs-feedback.ts
More file actions
122 lines (101 loc) · 3.26 KB
/
Copy pathdocs-feedback.ts
File metadata and controls
122 lines (101 loc) · 3.26 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
import type { VercelRequest, VercelResponse } from '@vercel/node'
import { sheets } from '@googleapis/sheets'
import { GoogleAuth } from 'google-auth-library'
interface FeedbackBody {
page_url: string
rating: 'yes' | 'no'
option?: string
reason?: string
}
function stripHtml(text: string): string {
let prev = text
while (true) {
const next = prev.replace(/<[^>]*>/g, '')
if (next === prev) return next
prev = next
}
}
function sanitize(text: string): string {
return stripHtml(text)
.replace(/!\[([^\]]*)\]\([^)]*\)/g, '$1')
.replace(/\[([^\]]*)\]\([^)]*\)/g, '$1')
.replace(/@/g, '@\u200B')
.replace(/#(\d)/g, '#\u200B$1')
.slice(0, 1000)
.trim()
}
function isValidPageUrl(url: string): boolean {
if (url.startsWith('/')) return /^\/[\w\-./]*$/.test(url)
try {
const parsed = new URL(url)
return parsed.origin === 'https://docs.metamask.io'
} catch {
return false
}
}
function getDeviceType(ua: string): 'mobile' | 'desktop' {
return /Mobile|Android|iPhone|iPad/i.test(ua) ? 'mobile' : 'desktop'
}
const credentials = JSON.parse(
Buffer.from(process.env.GOOGLE_SHEETS_CREDENTIALS!, 'base64').toString()
)
const auth = new GoogleAuth({
credentials,
scopes: ['https://www.googleapis.com/auth/spreadsheets'],
})
const sheetsClient = sheets({ version: 'v4', auth })
async function appendToSheet(row: string[]) {
await sheetsClient.spreadsheets.values.append({
spreadsheetId: process.env.GOOGLE_SHEET_ID!,
range: 'Sheet1!A:E',
valueInputOption: 'RAW',
insertDataOption: 'INSERT_ROWS',
requestBody: { values: [row] },
})
}
export default async function handler(req: VercelRequest, res: VercelResponse) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' })
}
if (!req.body || typeof req.body !== 'object') {
return res.status(400).json({ error: 'Invalid or missing JSON body' })
}
const { page_url: pageUrl, rating, option, reason } = req.body as Partial<FeedbackBody>
if (
typeof pageUrl !== 'string' ||
!pageUrl ||
typeof rating !== 'string' ||
!['yes', 'no'].includes(rating)
) {
return res.status(400).json({ error: 'page_url and rating (yes/no) are required' })
}
if (option !== undefined && typeof option !== 'string') {
return res.status(400).json({ error: 'option must be a string' })
}
if (reason !== undefined && typeof reason !== 'string') {
return res.status(400).json({ error: 'reason must be a string' })
}
if (!isValidPageUrl(pageUrl)) {
return res.status(400).json({ error: 'invalid page_url' })
}
const cleanOption = option ? sanitize(option) : ''
const cleanReason = reason ? sanitize(reason) : ''
const feedbackDetail = cleanReason || cleanOption
if (rating === 'no' && !feedbackDetail) {
return res.status(400).json({ error: 'option or reason is required for negative feedback' })
}
const ts = new Date().toISOString()
try {
await appendToSheet([
ts,
pageUrl,
rating,
feedbackDetail,
getDeviceType((req.headers['user-agent'] as string) ?? ''),
])
} catch (err) {
console.error('Google Sheets append failed:', err)
return res.status(500).json({ error: 'Failed to save feedback' })
}
return res.status(200).json({ ok: true })
}