@@ -6,6 +6,7 @@ package frontend
66
77import (
88 "context"
9+ "encoding/json"
910 "errors"
1011 "fmt"
1112 "net/http"
@@ -247,11 +248,17 @@ func fetchSearchPage(ctx context.Context, ds internal.DataSource, cq, symbol str
247248 pageParams paginationParams , searchSymbols bool , vulnClient * vuln.Client , embeddingsClient VectorEmbedder ) (* SearchPage , error ) {
248249 maxResultCount := maxSearchOffset + pageParams .limit
249250
250- var vec []float32
251+ var (
252+ vec []float32
253+ embeddingLatency time.Duration
254+ searchLatency time.Duration
255+ )
251256 if embeddingsClient != nil && ! searchSymbols && strings .TrimSpace (cq ) != "" && experiment .IsActive (ctx , internal .ExperimentVectorSearch ) {
252257 embedCtx , cancel := context .WithTimeout (ctx , searchEmbeddingTimeout )
253258 defer cancel ()
259+ startEmbed := time .Now ()
254260 vecs , err := embeddingsClient .GenerateEmbeddings (embedCtx , []string {cq }, "RETRIEVAL_QUERY" )
261+ embeddingLatency = time .Since (startEmbed )
255262 if err != nil {
256263 log .Errorf (ctx , "failed to generate query vector for %q: %v" , cq , err )
257264 } else if len (vecs ) > 0 {
@@ -261,6 +268,7 @@ func fetchSearchPage(ctx context.Context, ds internal.DataSource, cq, symbol str
261268
262269 // Pageless search: always start from the beginning.
263270 offset := 0
271+ startSearch := time .Now ()
264272 dbresults , err := ds .Search (ctx , cq , internal.SearchOptions {
265273 MaxResults : pageParams .limit ,
266274 Offset : offset ,
@@ -270,6 +278,7 @@ func fetchSearchPage(ctx context.Context, ds internal.DataSource, cq, symbol str
270278 GroupResults : true ,
271279 Vector : vec ,
272280 })
281+ searchLatency = time .Since (startSearch )
273282 if err != nil {
274283 return nil , err
275284 }
@@ -299,6 +308,19 @@ func fetchSearchPage(ctx context.Context, ds internal.DataSource, cq, symbol str
299308 numPageResults += 1 + len (r .SameModule )
300309 }
301310
311+ cohort := "control"
312+ if len (vec ) > 0 {
313+ cohort = "treatment"
314+ }
315+ log .Info (ctx , map [string ]any {
316+ "log_type" : "search_query_execution" ,
317+ "query" : cq ,
318+ "cohort" : cohort ,
319+ "embedding_latency_ms" : embeddingLatency .Milliseconds (),
320+ "search_latency_ms" : searchLatency .Milliseconds (),
321+ "num_results" : numResults ,
322+ })
323+
302324 pgs := newPagination (pageParams , numPageResults , numResults )
303325 sp := & SearchPage {
304326 PackageTabQuery : cq ,
@@ -604,5 +626,47 @@ func addVulns(ctx context.Context, rs []*SearchResult, vc *vuln.Client) {
604626 })
605627 }
606628 wg .Wait ()
629+ }
630+
631+ type searchClickPayload struct {
632+ Query string `json:"query"`
633+ ClickedPackage string `json:"clicked_package"`
634+ Rank int `json:"rank"`
635+ Cohort string `json:"cohort"`
636+ Timestamp string `json:"timestamp"`
637+ }
638+
639+ func (s * Server ) handleSearchClick (w http.ResponseWriter , r * http.Request ) {
640+ if r .Method != http .MethodPost {
641+ http .Error (w , "method not allowed" , http .StatusMethodNotAllowed )
642+ return
643+ }
607644
645+ r .Body = http .MaxBytesReader (w , r .Body , 10 << 10 )
646+
647+ var payload searchClickPayload
648+ if err := json .NewDecoder (r .Body ).Decode (& payload ); err != nil {
649+ http .Error (w , "bad request" , http .StatusBadRequest )
650+ return
651+ }
652+
653+ // Input validation: require non-empty fields & valid cohort.
654+ if strings .TrimSpace (payload .Query ) == "" || strings .TrimSpace (payload .ClickedPackage ) == "" || payload .Rank < 0 {
655+ http .Error (w , "missing or invalid required fields" , http .StatusBadRequest )
656+ return
657+ }
658+ if payload .Cohort != "control" && payload .Cohort != "treatment" {
659+ http .Error (w , "invalid cohort" , http .StatusBadRequest )
660+ return
661+ }
662+
663+ log .Info (r .Context (), map [string ]any {
664+ "log_type" : "search_click_telemetry" ,
665+ "query" : payload .Query ,
666+ "clicked_package" : payload .ClickedPackage ,
667+ "rank" : payload .Rank ,
668+ "cohort" : payload .Cohort ,
669+ "timestamp" : payload .Timestamp ,
670+ })
671+ w .WriteHeader (http .StatusNoContent )
608672}
0 commit comments