Files
signoz/pkg/errors/suggestions.go
Tushar Vats 629ea3b8be
Some checks failed
build-staging / js-build (push) Has been cancelled
build-staging / prepare (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
feat: extend error responses with new error struct (#11542)
* feat: extend error responses with new error struct

* fix: enriched error for dashboard api

* fix: merge issues

* fix: reverted dashboards changes and add for cloud integrations

* fix: delete file

* fix: add back file

* fix: added a helper

* fix: removed invlaid referencess

* fix: generate openapi

* fix: keeping additional along with suggestion

* Revert "fix: keeping additional along with suggestion"

This reverts commit be30e2ffd2.

* fix: added suggestions per additonal error

* fix: generate openapi

* fix: remove valid references

* fix: removeg valid references for select and group by and only did you mean is kept

* fix: unit test

* fix: use binding for deconding for both ee and community

* fix: trim down suggestions methods

* fix: added renamed methods and moved stuff around

* fix: typo

* fix: removed json decoder

* fix: added empty check

* fix: retain addtional

* fix: reverted re-structing of file
2026-06-15 11:58:12 +00:00

166 lines
3.9 KiB
Go

package errors
import (
"fmt"
"strings"
)
const (
typoSuggestionThreshold = 0.75
// maxValidReferences caps how many valid references are listed so
// high-cardinality sets (e.g. telemetry field keys) don't dump the entire
// set into the error.
maxValidReferences = 20
)
// SuggestionsOnLevenshteinDistance returns a "did you mean" correction (only
// when a close match at least typoSuggestionThreshold similar exists) followed
// by the valid-references list.
func SuggestionsOnLevenshteinDistance(invalidInput string, validInputs []string) []string {
suggestions := make([]string, 0, 2)
if match, ok := ClosestLevenshteinMatch(invalidInput, validInputs); ok {
suggestions = append(suggestions, didYouMean(match))
}
if refs := ValidReferences(validInputs...); refs != "" {
suggestions = append(suggestions, refs)
}
return suggestions
}
// ClosestLevenshteinMatch returns the candidate most similar to input that is at least
// typoSuggestionThreshold similar, or false when nothing is close enough.
func ClosestLevenshteinMatch(input string, candidates []string) (string, bool) {
var bestMatch string
bestSimilarity := 0.0
for _, candidate := range candidates {
sim := similarity(input, candidate)
if sim > bestSimilarity && sim >= typoSuggestionThreshold {
bestSimilarity = sim
bestMatch = candidate
}
}
if bestSimilarity >= typoSuggestionThreshold {
return bestMatch, true
}
return "", false
}
// SuggestionsFromFunc formats the string produce returns as a one-element
// "did you mean: `x`" slice, or nil when it returns the empty string (so callers
// with their own matching strategy compose into a suggestions list cleanly).
func SuggestionsFromFunc(produce func() string) []string {
s := produce()
if s == "" {
return nil
}
return []string{didYouMean(s)}
}
// ValidReferences formats values as "valid references: `a`, `b`", capped at
// maxValidReferences with a "(+N more)" suffix. Each value is rendered as its
// own string, an Enum() element's StringValue(), or fmt.Sprint as a fallback.
// It returns "" when there are no values, so callers don't surface a bare
// "valid references: " with nothing after it.
func ValidReferences[T any](values ...T) string {
if len(values) == 0 {
return ""
}
refs := make([]string, 0, len(values))
for _, v := range values {
switch t := any(v).(type) {
case string:
refs = append(refs, t)
case interface{ StringValue() string }:
refs = append(refs, t.StringValue())
default:
refs = append(refs, fmt.Sprint(t))
}
}
truncated := 0
if len(refs) > maxValidReferences {
truncated = len(refs) - maxValidReferences
refs = refs[:maxValidReferences]
}
quoted := make([]string, len(refs))
for i, r := range refs {
quoted[i] = "`" + r + "`"
}
out := "valid references: " + strings.Join(quoted, ", ")
if truncated > 0 {
out += fmt.Sprintf(" (+%d more)", truncated)
}
return out
}
func levenshteinDistance(s1, s2 string) int {
s1 = strings.ToLower(s1)
s2 = strings.ToLower(s2)
if len(s1) == 0 {
return len(s2)
}
if len(s2) == 0 {
return len(s1)
}
v0 := make([]int, len(s2)+1)
v1 := make([]int, len(s2)+1)
for i := 0; i <= len(s2); i++ {
v0[i] = i
}
for i := range len(s1) {
v1[0] = i + 1
for j := range len(s2) {
deletionCost := v0[j+1] + 1
insertionCost := v1[j] + 1
var substitutionCost int
if s1[i] == s2[j] {
substitutionCost = v0[j]
} else {
substitutionCost = v0[j] + 1
}
v1[j+1] = min(deletionCost, insertionCost, substitutionCost)
}
for j := 0; j <= len(s2); j++ {
v0[j] = v1[j]
}
}
return v1[len(s2)]
}
func similarity(s1, s2 string) float64 {
maxLen := max(len(s1), len(s2))
if maxLen == 0 {
return 1.0
}
distance := levenshteinDistance(s1, s2)
return 1.0 - float64(distance)/float64(maxLen)
}
// didYouMean formats a correction as "did you mean: `x`".
func didYouMean(match string) string {
return "did you mean: `" + match + "`"
}