1
1
import { Request , Response } from 'express'
2
+ import statsd from '@/observability/lib/statsd'
2
3
import got from 'got'
3
4
import { getHmacWithEpoch } from '@/search/lib/helpers/get-cse-copilot-auth'
4
- import { getCSECopilotSource } from '#src /search/lib/helpers/cse-copilot-docs-versions.js '
5
+ import { getCSECopilotSource } from '@ /search/lib/helpers/cse-copilot-docs-versions'
5
6
6
7
const memoryCache = new Map < string , Buffer > ( )
7
8
8
9
export const aiSearchProxy = async ( req : Request , res : Response ) => {
9
10
const { query, version, language } = req . body
11
+
10
12
const errors = [ ]
11
13
12
14
// Validate request body
@@ -34,13 +36,25 @@ export const aiSearchProxy = async (req: Request, res: Response) => {
34
36
return
35
37
}
36
38
39
+ const diagnosticTags = [
40
+ `version:${ version } ` . slice ( 0 , 200 ) ,
41
+ `language:${ language } ` . slice ( 0 , 200 ) ,
42
+ `queryLength:${ query . length } ` . slice ( 0 , 200 ) ,
43
+ ]
44
+ statsd . increment ( 'ai-search.call' , 1 , diagnosticTags )
45
+
46
+ // TODO: Caching here may cause an issue if the cache grows too large. Additionally, the cache will be inconsistent across pods
37
47
const cacheKey = `${ query } :${ version } :${ language } `
38
48
if ( memoryCache . has ( cacheKey ) ) {
49
+ statsd . increment ( 'ai-search.cache_hit' , 1 , diagnosticTags )
39
50
res . setHeader ( 'Content-Type' , 'application/x-ndjson' )
40
51
res . send ( memoryCache . get ( cacheKey ) )
41
52
return
42
53
}
43
54
55
+ const startTime = Date . now ( )
56
+ let totalChars = 0
57
+
44
58
const body = {
45
59
chat_context : 'docs' ,
46
60
docs_source : docsSource ,
@@ -57,22 +71,19 @@ export const aiSearchProxy = async (req: Request, res: Response) => {
57
71
} ,
58
72
} )
59
73
60
- const chunks : Buffer [ ] = [ ]
61
- stream . on ( 'data' , ( chunk ) => {
62
- chunks . push ( chunk )
74
+ // Listen for data events to count characters
75
+ stream . on ( 'data' , ( chunk : Buffer | string ) => {
76
+ // Ensure we have a string for proper character count
77
+ const dataStr = typeof chunk === 'string' ? chunk : chunk . toString ( )
78
+ totalChars += dataStr . length
63
79
} )
64
80
65
81
// Handle the upstream response before piping
66
82
stream . on ( 'response' , ( upstreamResponse ) => {
67
- // When cse-copilot returns a 204, it means the backend received the request
68
- // but was unable to answer the question. So we return a 400 to the client to be handled.
69
- if ( upstreamResponse . statusCode === 204 ) {
70
- return res
71
- . status ( 400 )
72
- . json ( { errors : [ { message : 'Sorry I am unable to answer this question.' } ] } )
73
- } else if ( upstreamResponse . statusCode !== 200 ) {
83
+ if ( upstreamResponse . statusCode !== 200 ) {
74
84
const errorMessage = `Upstream server responded with status code ${ upstreamResponse . statusCode } `
75
85
console . error ( errorMessage )
86
+ statsd . increment ( 'ai-search.stream_response_error' , 1 , diagnosticTags )
76
87
res . status ( 500 ) . json ( { errors : [ { message : errorMessage } ] } )
77
88
stream . destroy ( )
78
89
} else {
@@ -95,6 +106,8 @@ export const aiSearchProxy = async (req: Request, res: Response) => {
95
106
. json ( { errors : [ { message : 'Sorry I am unable to answer this question.' } ] } )
96
107
}
97
108
109
+ statsd . increment ( 'ai-search.stream_error' , 1 , diagnosticTags )
110
+
98
111
if ( ! res . headersSent ) {
99
112
res . status ( 500 ) . json ( { errors : [ { message : 'Internal server error' } ] } )
100
113
} else {
@@ -106,12 +119,19 @@ export const aiSearchProxy = async (req: Request, res: Response) => {
106
119
}
107
120
} )
108
121
109
- // Ensure response ends when stream ends
122
+ // Calculate metrics on stream end
110
123
stream . on ( 'end' , ( ) => {
111
- memoryCache . set ( cacheKey , Buffer . concat ( chunks as Uint8Array [ ] ) )
124
+ const totalResponseTime = Date . now ( ) - startTime // in ms
125
+ const charPerMsRatio = totalResponseTime > 0 ? totalChars / totalResponseTime : 0 // chars per ms
126
+
127
+ statsd . gauge ( 'ai-search.total_response_time' , totalResponseTime , diagnosticTags )
128
+ statsd . gauge ( 'ai-search.response_chars_per_ms' , charPerMsRatio , diagnosticTags )
129
+
130
+ statsd . increment ( 'ai-search.success_stream_end' , 1 , diagnosticTags )
112
131
res . end ( )
113
132
} )
114
133
} catch ( error ) {
134
+ statsd . increment ( 'ai-search.route_error' , 1 , diagnosticTags )
115
135
console . error ( 'Error posting /answers to cse-copilot:' , error )
116
136
res . status ( 500 ) . json ( { errors : [ { message : 'Internal server error' } ] } )
117
137
}
0 commit comments