@@ -20,17 +20,34 @@ type RecentError = {
2020} ;
2121
2222type FeedbackPayload = {
23+ schema_version ?: number ;
2324 session_id ?: string ;
2425 type ?: string ;
2526 content ?: string ;
2627 version ?: string ;
2728 os ?: string ;
2829 model ?: string ;
2930 recent_errors ?: RecentError [ ] ;
31+ session ?: Record < string , unknown > ;
32+ client ?: Record < string , unknown > ;
33+ repo ?: Record < string , unknown > ;
34+ context ?: {
35+ recent_errors ?: RecentError [ ] ;
36+ last_messages ?: unknown [ ] ;
37+ tool_calls ?: unknown [ ] ;
38+ subagents ?: unknown [ ] ;
39+ } ;
40+ privacy ?: Record < string , unknown > ;
41+ } ;
42+
43+ type GitHubIssue = {
44+ number ?: number ;
45+ html_url ?: string ;
3046} ;
3147
3248const MAX_CONTENT_LENGTH = 10_000 ;
3349const MAX_RECENT_ERRORS = 10 ;
50+ const MAX_CONTEXT_ITEMS = 20 ;
3451
3552export default {
3653 async fetch ( request : Request , env : Env ) : Promise < Response > {
@@ -50,15 +67,17 @@ export default {
5067
5168 let payload : FeedbackPayload ;
5269 try {
53- payload = await request . json < FeedbackPayload > ( ) ;
70+ payload = ( await request . json ( ) ) as FeedbackPayload ;
5471 } catch {
5572 return jsonResponse ( { error : "invalid_json" } , 400 ) ;
5673 }
5774
5875 const content = ( payload . content || "" ) . trim ( ) ;
59- const recentErrors = Array . isArray ( payload . recent_errors )
60- ? payload . recent_errors . slice ( 0 , MAX_RECENT_ERRORS )
61- : [ ] ;
76+ const recentErrors = Array . isArray ( payload . context ?. recent_errors )
77+ ? payload . context . recent_errors . slice ( 0 , MAX_RECENT_ERRORS )
78+ : Array . isArray ( payload . recent_errors )
79+ ? payload . recent_errors . slice ( 0 , MAX_RECENT_ERRORS )
80+ : [ ] ;
6281 if ( ! content && recentErrors . length === 0 ) {
6382 return jsonResponse ( { error : "empty_feedback" } , 400 ) ;
6483 }
@@ -74,15 +93,25 @@ export default {
7493 os : trim ( payload . os , 128 ) ,
7594 model : trim ( payload . model , 128 ) ,
7695 recent_errors : recentErrors . map ( sanitizeRecentError ) ,
96+ session : sanitizeRecord ( payload . session ) ,
97+ client : sanitizeRecord ( payload . client ) ,
98+ repo : sanitizeRecord ( payload . repo , 24 , 20_000 ) ,
99+ context : {
100+ recent_errors : recentErrors . map ( sanitizeRecentError ) ,
101+ last_messages : sanitizeArray ( payload . context ?. last_messages ) ,
102+ tool_calls : sanitizeArray ( payload . context ?. tool_calls ) ,
103+ subagents : sanitizeArray ( payload . context ?. subagents ) ,
104+ } ,
105+ privacy : sanitizeRecord ( payload . privacy ) ,
77106 } ;
78107
79108 const title = githubTitle ( sanitizedPayload ) ;
80109 const body = githubBody ( sanitizedPayload , request ) ;
81110
82- await createGithubIssue ( env , title , body ) ;
111+ const issue = await createGithubIssue ( env , sanitizedPayload , title , body ) ;
83112 await sendSupportEmail ( env , title , body ) ;
84113
85- return corsResponse ( null , 204 ) ;
114+ return jsonResponse ( { number : issue . number , html_url : issue . html_url } , 201 ) ;
86115 } ,
87116} ;
88117
@@ -103,6 +132,33 @@ function sanitizeRecentError(error: RecentError): RecentError {
103132 } ;
104133}
105134
135+ function sanitizeValue ( value : unknown , maxStringLength = 2_000 ) : unknown {
136+ if ( typeof value === "string" ) return value . slice ( 0 , maxStringLength ) ;
137+ if ( typeof value === "number" || typeof value === "boolean" || value === null ) return value ;
138+ if ( Array . isArray ( value ) ) return value . slice ( 0 , MAX_CONTEXT_ITEMS ) . map ( ( item ) => sanitizeValue ( item ) ) ;
139+ if ( typeof value === "object" && value !== null ) return sanitizeRecord ( value as Record < string , unknown > ) ;
140+ return undefined ;
141+ }
142+
143+ function sanitizeRecord (
144+ value : unknown ,
145+ maxKeys = 20 ,
146+ maxStringLength = 2_000 ,
147+ ) : Record < string , unknown > | undefined {
148+ if ( typeof value !== "object" || value === null || Array . isArray ( value ) ) return undefined ;
149+ const out : Record < string , unknown > = { } ;
150+ for ( const [ key , raw ] of Object . entries ( value ) . slice ( 0 , maxKeys ) ) {
151+ out [ key . slice ( 0 , 80 ) ] = sanitizeValue ( raw , maxStringLength ) ;
152+ }
153+ return out ;
154+ }
155+
156+ function sanitizeArray ( value : unknown ) : unknown [ ] | undefined {
157+ return Array . isArray ( value )
158+ ? value . slice ( 0 , MAX_CONTEXT_ITEMS ) . map ( ( item ) => sanitizeValue ( item ) )
159+ : undefined ;
160+ }
161+
106162function githubTitle ( payload : FeedbackPayload ) : string {
107163 const prefix = payload . type === "error" ? "Error report" : "Feedback" ;
108164 const version = payload . version ? ` ${ payload . version } ` : "" ;
@@ -127,6 +183,9 @@ function githubBody(payload: FeedbackPayload, request: Request): string {
127183 `- CF ray: ${ request . headers . get ( "cf-ray" ) || "unknown" } ` ,
128184 ] ;
129185
186+ appendJsonSection ( lines , "Privacy" , payload . privacy ) ;
187+ appendJsonSection ( lines , "Repository" , payload . repo ) ;
188+
130189 if ( payload . recent_errors ?. length ) {
131190 lines . push ( "" , "## Recent errors" , "" ) ;
132191 for ( const error of payload . recent_errors ) {
@@ -138,11 +197,30 @@ function githubBody(payload: FeedbackPayload, request: Request): string {
138197 }
139198 }
140199
200+ appendJsonSection ( lines , "Recent visible messages" , payload . context ?. last_messages ) ;
201+ appendJsonSection ( lines , "Tool calls" , payload . context ?. tool_calls ) ;
202+ appendJsonSection ( lines , "Subagents" , payload . context ?. subagents ) ;
203+
141204 return lines . join ( "\n" ) ;
142205}
143206
144- async function createGithubIssue ( env : Env , title : string , body : string ) : Promise < void > {
145- const labels = splitCsv ( env . GITHUB_LABELS || "feedback,pythinker-cli" ) ;
207+ function appendJsonSection ( lines : string [ ] , title : string , value : unknown ) : void {
208+ if ( value === undefined || value === null ) return ;
209+ if ( Array . isArray ( value ) && value . length === 0 ) return ;
210+ if ( typeof value === "object" && ! Array . isArray ( value ) && Object . keys ( value ) . length === 0 ) return ;
211+ lines . push ( "" , `## ${ title } ` , "" , "```json" , JSON . stringify ( value , null , 2 ) , "```" ) ;
212+ }
213+
214+ async function createGithubIssue (
215+ env : Env ,
216+ payload : FeedbackPayload ,
217+ title : string ,
218+ body : string ,
219+ ) : Promise < GitHubIssue > {
220+ const labels = unique ( [
221+ ...splitCsv ( env . GITHUB_LABELS || "feedback,pythinker-cli" ) ,
222+ `feedback:${ payload . type || "feedback" } ` ,
223+ ] ) ;
146224 const assignees = splitCsv ( env . GITHUB_ASSIGNEES || "" ) ;
147225 const response = await fetch ( `https://api.github.com/repos/${ env . GITHUB_REPO } /issues` , {
148226 method : "POST" ,
@@ -159,6 +237,8 @@ async function createGithubIssue(env: Env, title: string, body: string): Promise
159237 if ( ! response . ok ) {
160238 throw new Error ( `GitHub issue creation failed: ${ response . status } ` ) ;
161239 }
240+ const issue = ( await response . json ( ) ) as GitHubIssue ;
241+ return { number : issue . number , html_url : issue . html_url } ;
162242}
163243
164244async function sendSupportEmail ( env : Env , subject : string , body : string ) : Promise < void > {
@@ -229,6 +309,10 @@ function splitCsv(value: string): string[] {
229309 . filter ( Boolean ) ;
230310}
231311
312+ function unique ( values : string [ ] ) : string [ ] {
313+ return Array . from ( new Set ( values ) ) ;
314+ }
315+
232316function corsResponse ( body : BodyInit | null , status : number ) : Response {
233317 return new Response ( body , { status, headers : corsHeaders ( ) } ) ;
234318}
0 commit comments