From b1ee80f9b6af67595707414a25c17d0aef6d2d43 Mon Sep 17 00:00:00 2001 From: Shekhar Vaidya Date: Mon, 13 Jul 2026 11:27:12 +0530 Subject: [PATCH 1/4] fix(cli): use plain "English" in --lang examples, not "English (United States)" Consumer Windows products list their English option as "English" (or "English International"), not "English (United States)" -- the exact example this used, which is why it was such a common source of --lang mismatch errors. Applies to --help, the --lang flag description, and the release workflow's usage example. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/cli-release.yml | 2 +- cli/main.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cli-release.yml b/.github/workflows/cli-release.yml index 487fec5..0b14dc3 100644 --- a/.github/workflows/cli-release.yml +++ b/.github/workflows/cli-release.yml @@ -90,7 +90,7 @@ jobs: ``` msdl # fully interactive (includes Server/Enterprise) msdl "windows 11 25h2" # filter products, pick language - msdl --id 3262 --lang "English (United States)" # direct, no prompts + msdl --id 3262 --lang "English" # direct, no prompts msdl --eval server-2025 # evaluation ISOs directly msdl --list # list all products ``` diff --git a/cli/main.go b/cli/main.go index 5297457..75289f2 100644 --- a/cli/main.go +++ b/cli/main.go @@ -167,7 +167,7 @@ func run(args []string) error { Usage: msdl [search terms] interactive: filter + pick product, pick language msdl --id 3262 skip product picker - msdl --id 3262 --lang "English (United States)" no prompts, print URL directly + msdl --id 3262 --lang "English" no prompts, print URL directly (language names vary by product — omit --lang or check the error's "available" list to see them) msdl --eval [slug] evaluation ISOs (server-2025, win11-ent, ...) @@ -185,7 +185,7 @@ More info: https://msdl.tech-latest.com/cli`) } productID := fs.String("id", "", "consumer product ID (skips product picker)") - langFlag := fs.String("lang", "", `language name, e.g. "English (United States)"`) + langFlag := fs.String("lang", "", `language name, e.g. "English"`) evalMode := fs.Bool("eval", false, "fetch evaluation ISOs") listMode := fs.Bool("list", false, "list all products and exit") noContributeFlag := fs.Bool("no-contribute", false, "skip sharing the link with the msdl.tech cache") -- 2.45.3 From 3994a82d6dd16d4da30714f7109588c212b85c68 Mon Sep 17 00:00:00 2001 From: Shekhar Vaidya Date: Mon, 13 Jul 2026 12:24:43 +0530 Subject: [PATCH 2/4] feat(cli): homepage landing screen for bare msdl; fix search substring bug The bare `msdl` invocation showed all 22 products (17 consumer + 5 eval) as a flat numbered list. Replaces it with a curated landing screen: 5 popular products (picked from real usage telemetry) plus a 2-item eval shortlist, with "list" as an escape hatch to today's full picker and free text routed through the existing search flow. Also fixes a search bug: querying "windows 10" incorrectly matched Windows 11 24H2 because "10" is a substring of its build number (26100.1742). Query words now must start at a word boundary rather than matching anywhere, so digit fragments inside unrelated numbers can no longer collide -- while "arm" still fuzzy-matches "ARM64". Co-Authored-By: Claude Sonnet 4.6 --- cli/catalog.go | 27 ++++++- cli/catalog_test.go | 34 ++++++++- cli/homepage.go | 114 +++++++++++++++++++++++++++++ cli/homepage_test.go | 166 +++++++++++++++++++++++++++++++++++++++++++ cli/main.go | 73 +++++++++++++------ 5 files changed, 391 insertions(+), 23 deletions(-) create mode 100644 cli/homepage.go create mode 100644 cli/homepage_test.go diff --git a/cli/catalog.go b/cli/catalog.go index 9c949c2..ffa8188 100644 --- a/cli/catalog.go +++ b/cli/catalog.go @@ -60,7 +60,7 @@ func searchProducts(query string) []Product { name := strings.ToLower(p.Name) match := true for _, w := range words { - if !strings.Contains(name, w) { + if !containsWordStart(name, w) { match = false break } @@ -72,6 +72,31 @@ func searchProducts(query string) []Product { return results } +// containsWordStart reports whether s contains substr starting at a word +// boundary: the start of s, or right after a non-alphanumeric character. +// Plain strings.Contains would let a query word match a digit fragment +// buried inside an unrelated number -- e.g. "10" matching inside a build +// number like "26100.1742" -- which made "windows 10" incorrectly return +// Windows 11 24H2 results. +func containsWordStart(s, substr string) bool { + from := 0 + for { + i := strings.Index(s[from:], substr) + if i < 0 { + return false + } + pos := from + i + if pos == 0 || !isAlphanumeric(s[pos-1]) { + return true + } + from = pos + 1 + } +} + +func isAlphanumeric(b byte) bool { + return (b >= 'a' && b <= 'z') || (b >= '0' && b <= '9') +} + func findEvalProduct(slug string) (EvalProduct, bool) { for _, p := range evalProducts { if p.Slug == slug { diff --git a/cli/catalog_test.go b/cli/catalog_test.go index fa316dc..09be26d 100644 --- a/cli/catalog_test.go +++ b/cli/catalog_test.go @@ -1,6 +1,9 @@ package main -import "testing" +import ( + "strings" + "testing" +) func TestFindProductByID_found(t *testing.T) { p, ok := findProductByID("3262") @@ -40,6 +43,35 @@ func TestSearchProducts_caseInsensitive(t *testing.T) { } } +func TestSearchProducts_noDigitFragmentCollision(t *testing.T) { + // "10" must not match inside the "26100.1742" build number of Windows 11 + // 24H2/25H2 products -- regression test for the reported "windows 10" + // bug that returned Windows 11 results. + results := searchProducts("windows 10") + for _, p := range results { + if strings.Contains(p.Name, "11") { + t.Errorf("query %q matched %q, a Windows 11 product", "windows 10", p.Name) + } + } + if len(results) == 0 { + t.Fatal("expected windows 10 to match the actual Windows 10 products") + } +} + +func TestSearchProducts_prefixSubstringStillWorks(t *testing.T) { + // "arm" should still fuzzy-match "ARM64" -- the word-boundary fix must + // only reject matches that start mid-digit-run, not all substrings. + results := searchProducts("arm") + if len(results) == 0 { + t.Fatal("expected arm to match ARM64 products") + } + for _, p := range results { + if !strings.Contains(strings.ToLower(p.Name), "arm") { + t.Errorf("unexpected match %q for query %q", p.Name, "arm") + } + } +} + func TestFindEvalProduct_found(t *testing.T) { p, ok := findEvalProduct("server-2025") if !ok { diff --git a/cli/homepage.go b/cli/homepage.go new file mode 100644 index 0000000..e57b006 --- /dev/null +++ b/cli/homepage.go @@ -0,0 +1,114 @@ +package main + +import ( + "bufio" + "fmt" + "os" + "strconv" + "strings" +) + +// homepageFeaturedIDs are the consumer products shown on the bare `msdl` +// landing screen, picked from real usage telemetry rather than the full +// catalog. The full catalog is still one step away via "list". +var homepageFeaturedIDs = []string{"3262", "2618", "3321", "3113", "3265"} + +// homepageEvalSlugs are the eval products shown on the landing screen. +var homepageEvalSlugs = []string{"server-2025", "win11-ent"} + +type homepageKind int + +const ( + homepageKindProduct homepageKind = iota + homepageKindEval + homepageKindList + homepageKindSearch +) + +type homepageChoice struct { + kind homepageKind + productID string + evalSlug string + query string +} + +const homepageRule = "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + +// showHomepage prints the landing screen for a bare `msdl` invocation and +// reads the user's choice: a number, "list" for the full catalog, or free +// text to search by name. +func showHomepage(latestVersion string) (homepageChoice, error) { + fmt.Fprintln(os.Stderr, homepageRule) + fmt.Fprintln(os.Stderr, "msdl — Windows ISO Downloader") + fmt.Fprintln(os.Stderr, "Direct links from Microsoft's CDN. No browser, no Media Creation Tool.") + fmt.Fprintln(os.Stderr, homepageRule) + fmt.Fprintln(os.Stderr) + + fmt.Fprintln(os.Stderr, "Popular:") + featured := make([]Product, 0, len(homepageFeaturedIDs)) + for _, id := range homepageFeaturedIDs { + if p, ok := findProductByID(id); ok { + featured = append(featured, p) + } + } + for i, p := range featured { + fmt.Fprintf(os.Stderr, "%d. %s\n", i+1, p.Name) + } + fmt.Fprintln(os.Stderr) + + fmt.Fprintln(os.Stderr, "Evaluation / Enterprise:") + evalShortlist := make([]EvalProduct, 0, len(homepageEvalSlugs)) + for _, slug := range homepageEvalSlugs { + if ep, ok := findEvalProduct(slug); ok { + evalShortlist = append(evalShortlist, ep) + } + } + for i, ep := range evalShortlist { + fmt.Fprintf(os.Stderr, "%d. %s\n", len(featured)+i+1, ep.Name) + } + fmt.Fprintln(os.Stderr) + + fmt.Fprintln(os.Stderr, `Commands: --list (full catalog) · --help`) + fmt.Fprintln(os.Stderr) + fmt.Fprintln(os.Stderr, homepageRule) + if latestVersion != "" && latestVersion != Version && Version != "dev" { + fmt.Fprintf(os.Stderr, "Version %s (Update Available %s) - a TechLatest Open-source Contribution\n", Version, latestVersion) + } else { + fmt.Fprintf(os.Stderr, "Version %s - a TechLatest Open-source Contribution\n", Version) + } + fmt.Fprintln(os.Stderr) + + max := len(featured) + len(evalShortlist) + fmt.Fprintf(os.Stderr, `Choice [1-%d], search by name, or "list": `, max) + + scanner := bufio.NewScanner(os.Stdin) + if !scanner.Scan() { + if err := scanner.Err(); err != nil { + return homepageChoice{}, fmt.Errorf("reading input: %w", err) + } + return homepageChoice{}, fmt.Errorf("no input") + } + return parseHomepageInput(scanner.Text(), featured, evalShortlist) +} + +// parseHomepageInput interprets the raw input line from the homepage prompt. +func parseHomepageInput(input string, featured []Product, evalShortlist []EvalProduct) (homepageChoice, error) { + text := strings.TrimSpace(input) + if text == "" { + return homepageChoice{}, fmt.Errorf(`enter a choice, search term, or "list"`) + } + if strings.EqualFold(text, "list") { + return homepageChoice{kind: homepageKindList}, nil + } + if n, err := strconv.Atoi(text); err == nil { + max := len(featured) + len(evalShortlist) + if n < 1 || n > max { + return homepageChoice{}, fmt.Errorf(`enter a number between 1 and %d, a search term, or "list"`, max) + } + if n <= len(featured) { + return homepageChoice{kind: homepageKindProduct, productID: featured[n-1].ID}, nil + } + return homepageChoice{kind: homepageKindEval, evalSlug: evalShortlist[n-len(featured)-1].Slug}, nil + } + return homepageChoice{kind: homepageKindSearch, query: text}, nil +} diff --git a/cli/homepage_test.go b/cli/homepage_test.go new file mode 100644 index 0000000..a3c1fbc --- /dev/null +++ b/cli/homepage_test.go @@ -0,0 +1,166 @@ +package main + +import ( + "bytes" + "io" + "os" + "strings" + "testing" +) + +func testFeatured() []Product { + return []Product{ + {ID: "3262", Name: "Windows 11 25H2 (26200.6584)"}, + {ID: "2618", Name: "Windows 10 22H2 v1 (19045.2965)"}, + } +} + +func testEvalShortlist() []EvalProduct { + return []EvalProduct{ + {Slug: "server-2025", Name: "Windows Server 2025"}, + {Slug: "win11-ent", Name: "Windows 11 Enterprise"}, + } +} + +func TestParseHomepageInput_product(t *testing.T) { + choice, err := parseHomepageInput("1", testFeatured(), testEvalShortlist()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if choice.kind != homepageKindProduct || choice.productID != "3262" { + t.Errorf("got %+v, want product 3262", choice) + } +} + +func TestParseHomepageInput_eval(t *testing.T) { + choice, err := parseHomepageInput("3", testFeatured(), testEvalShortlist()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if choice.kind != homepageKindEval || choice.evalSlug != "server-2025" { + t.Errorf("got %+v, want eval server-2025", choice) + } +} + +func TestParseHomepageInput_evalSecond(t *testing.T) { + choice, err := parseHomepageInput("4", testFeatured(), testEvalShortlist()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if choice.kind != homepageKindEval || choice.evalSlug != "win11-ent" { + t.Errorf("got %+v, want eval win11-ent", choice) + } +} + +func TestParseHomepageInput_outOfRange(t *testing.T) { + _, err := parseHomepageInput("5", testFeatured(), testEvalShortlist()) + if err == nil { + t.Fatal("expected error for out-of-range choice") + } +} + +func TestParseHomepageInput_listCaseInsensitive(t *testing.T) { + for _, s := range []string{"list", "List", "LIST"} { + choice, err := parseHomepageInput(s, testFeatured(), testEvalShortlist()) + if err != nil { + t.Fatalf("unexpected error for %q: %v", s, err) + } + if choice.kind != homepageKindList { + t.Errorf("input %q: got kind %v, want list", s, choice.kind) + } + } +} + +func TestParseHomepageInput_searchFallback(t *testing.T) { + choice, err := parseHomepageInput("windows 10", testFeatured(), testEvalShortlist()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if choice.kind != homepageKindSearch || choice.query != "windows 10" { + t.Errorf("got %+v, want search query \"windows 10\"", choice) + } +} + +func TestParseHomepageInput_emptyInput(t *testing.T) { + _, err := parseHomepageInput(" ", testFeatured(), testEvalShortlist()) + if err == nil { + t.Fatal("expected error for empty input") + } +} + +// captureHomepage runs showHomepage with stdin/stderr redirected to pipes so +// the rendered output and the parsed choice can both be inspected without a +// real terminal. +func captureHomepage(t *testing.T, stdin string, latestVersion string) (homepageChoice, string) { + t.Helper() + oldStdin, oldStderr := os.Stdin, os.Stderr + defer func() { os.Stdin, os.Stderr = oldStdin, oldStderr }() + + inR, inW, err := os.Pipe() + if err != nil { + t.Fatal(err) + } + os.Stdin = inR + inW.WriteString(stdin) + inW.Close() + + outR, outW, err := os.Pipe() + if err != nil { + t.Fatal(err) + } + os.Stderr = outW + + choice, hpErr := showHomepage(latestVersion) + outW.Close() + + var buf bytes.Buffer + io.Copy(&buf, outR) + if hpErr != nil { + t.Fatalf("unexpected error: %v", hpErr) + } + return choice, buf.String() +} + +func TestShowHomepage_rendersFeaturedAndEval(t *testing.T) { + choice, output := captureHomepage(t, "1\n", "") + if choice.kind != homepageKindProduct || choice.productID != "3262" { + t.Errorf("got %+v, want product 3262", choice) + } + for _, want := range []string{ + "1. Windows 11 25H2 (26200.6584)", + "2. Windows 10 22H2 v1 (19045.2965)", + "6. Windows Server 2025", + "7. Windows 11 Enterprise", + `Commands: --list (full catalog) · --help`, + `Choice [1-7], search by name, or "list":`, + } { + if !strings.Contains(output, want) { + t.Errorf("output missing %q, got:\n%s", want, output) + } + } +} + +func TestShowHomepage_updateAvailableFooter(t *testing.T) { + oldVersion := Version + Version = "0.3.3" + defer func() { Version = oldVersion }() + + _, output := captureHomepage(t, "1\n", "0.3.4") + if !strings.Contains(output, "Version 0.3.3 (Update Available 0.3.4)") { + t.Errorf("output missing update-available footer, got:\n%s", output) + } +} + +func TestShowHomepage_noUpdateFooter(t *testing.T) { + oldVersion := Version + Version = "0.3.4" + defer func() { Version = oldVersion }() + + _, output := captureHomepage(t, "1\n", "0.3.4") + if strings.Contains(output, "Update Available") { + t.Errorf("footer should not mention an update when already current, got:\n%s", output) + } + if !strings.Contains(output, "Version 0.3.4 - a TechLatest Open-source Contribution") { + t.Errorf("output missing plain version footer, got:\n%s", output) + } +} diff --git a/cli/main.go b/cli/main.go index 75289f2..8e47e0e 100644 --- a/cli/main.go +++ b/cli/main.go @@ -46,9 +46,9 @@ func contributeURL() string { return apiBaseURL() + "/contribute" } -// printUpdateNotice checks /cli/version and prints a notice if a newer version -// is available. Blocks up to 500ms; silently no-ops on timeout or any error. -func printUpdateNotice() { +// fetchLatestVersion fetches /cli/version and returns the latest version +// string, or "" on any error or after a 500ms timeout. +func fetchLatestVersion() string { ch := make(chan string, 1) go func() { ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) @@ -72,11 +72,18 @@ func printUpdateNotice() { }() select { case latest := <-ch: - if latest != "" && latest != Version && Version != "dev" { - fmt.Fprintf(os.Stderr, "\n A new version of msdl is available: v%s\n", latest) - fmt.Fprintf(os.Stderr, " Download: https://github.com/starkSV/windows-iso-downloader/releases/latest\n\n") - } + return latest case <-time.After(500 * time.Millisecond): + return "" + } +} + +// printUpdateNotice prints a notice if latest is newer than the running +// binary's version. +func printUpdateNotice(latest string) { + if latest != "" && latest != Version && Version != "dev" { + fmt.Fprintf(os.Stderr, "\n A new version of msdl is available: v%s\n", latest) + fmt.Fprintf(os.Stderr, " Download: https://github.com/starkSV/windows-iso-downloader/releases/latest\n\n") } } @@ -199,10 +206,17 @@ More info: https://msdl.tech-latest.com/cli`) query := strings.Join(fs.Args(), " ") noContribute := *noContributeFlag || os.Getenv("MSDL_NO_CONTRIBUTE") == "1" noTelemetry := os.Getenv("MSDL_NO_TELEMETRY") == "1" + isHomepage := *productID == "" && !*evalMode && !*listMode && query == "" && isTerminal() - // Update check — blocks up to 500ms, then continues regardless + // Update check — blocks up to 500ms, then continues regardless. The + // homepage screen folds this into its own footer instead of printing a + // separate notice. + latest := "" if !noTelemetry { - printUpdateNotice() + latest = fetchLatestVersion() + if !isHomepage { + printUpdateNotice(latest) + } } if *listMode { @@ -239,20 +253,37 @@ More info: https://msdl.tech-latest.com/cli`) telAction = "fetch" telProductID = *productID err = runConsumer(*productID, query, *langFlag, noContribute) - case query == "" && isTerminal(): - // Pure interactive: combined consumer + eval picker - isEval, product, ep, pickErr := pickCombined() - if pickErr != nil { - return pickErr + case isHomepage: + choice, hpErr := showHomepage(latest) + if hpErr != nil { + return hpErr } - if isEval { - telAction = "eval" - telEvalSlug = ep.Slug - err = runEval(ep.Slug) - } else { + switch choice.kind { + case homepageKindProduct: telAction = "fetch" - telProductID = product.ID - err = runConsumer(product.ID, "", *langFlag, noContribute) + telProductID = choice.productID + err = runConsumer(choice.productID, "", *langFlag, noContribute) + case homepageKindEval: + telAction = "eval" + telEvalSlug = choice.evalSlug + err = runEval(choice.evalSlug) + case homepageKindSearch: + telAction = "fetch" + err = runConsumer("", choice.query, *langFlag, noContribute) + case homepageKindList: + isEval, product, ep, pickErr := pickCombined() + if pickErr != nil { + return pickErr + } + if isEval { + telAction = "eval" + telEvalSlug = ep.Slug + err = runEval(ep.Slug) + } else { + telAction = "fetch" + telProductID = product.ID + err = runConsumer(product.ID, "", *langFlag, noContribute) + } } default: err = runConsumer(*productID, query, *langFlag, noContribute) -- 2.45.3 From b252ba1c30de753cfb3f6f722eeda96d6138c91e Mon Sep 17 00:00:00 2001 From: Shekhar Vaidya Date: Mon, 13 Jul 2026 12:37:01 +0530 Subject: [PATCH 3/4] feat(backend): approximate distinct-source count for Sentinel rejections CLI telemetry showed a growing count of Sentinel rejections but no way to tell whether they come from one user retrying repeatedly or many distinct users independently getting blocked. Adds a per-day Redis HyperLogLog (msdl:telemetry:sentinel_hll:) seeded from the reporting IP whenever a CLI error contains "Sentinel", plus a parallel daily counter. /metrics now exposes sentinel_today: {date, errors_today, distinct_sources_est}. No raw IPs are stored -- HLL is a one-way cardinality estimator, and both keys expire after 48h. Co-Authored-By: Claude Sonnet 4.6 --- backend/main.go | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/backend/main.go b/backend/main.go index 3fa9785..e4a7b7c 100644 --- a/backend/main.go +++ b/backend/main.go @@ -1675,6 +1675,20 @@ func handleTelemetry(w http.ResponseWriter, r *http.Request) { rdb.HIncrBy(ctx, "msdl:telemetry:results", result, 1) if !p.Success && p.Error != "" && len(p.Error) <= 200 { rdb.HIncrBy(ctx, "msdl:telemetry:errors", p.Error, 1) + if strings.Contains(p.Error, "Sentinel") { + // Track today's Sentinel-rejection count and an approximate + // distinct-source count (via HyperLogLog on the reporting + // IP) so /metrics can show whether rejections are + // concentrated in a few sources or spread across many, + // without ever storing raw IPs. + day := time.Now().UTC().Format("2006-01-02") + countKey := "msdl:telemetry:sentinel_count:" + day + hllKey := "msdl:telemetry:sentinel_hll:" + day + rdb.Incr(ctx, countKey) + rdb.Expire(ctx, countKey, 48*time.Hour) + rdb.PFAdd(ctx, hllKey, ip) + rdb.Expire(ctx, hllKey, 48*time.Hour) + } } } @@ -1775,6 +1789,7 @@ func handleMetrics(w http.ResponseWriter, r *http.Request) { evalCacheMu.RUnlock() telemetry := loadTelemetryFromRedis() + sentinelToday := loadSentinelTodayFromRedis() w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ @@ -1805,9 +1820,29 @@ func handleMetrics(w http.ResponseWriter, r *http.Request) { "neg_cache_size": negSize, "total_ms_fetches": skuFetches + linkFetches, "telemetry": telemetry, + "sentinel_today": sentinelToday, }) } +// loadSentinelTodayFromRedis returns today's Sentinel-rejection count and an +// approximate distinct-source count (from the HyperLogLog seeded in +// handleTelemetry). Returns nil when Redis is unavailable. +func loadSentinelTodayFromRedis() map[string]interface{} { + if rdb == nil { + return nil + } + ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) + defer cancel() + day := time.Now().UTC().Format("2006-01-02") + count, _ := rdb.Get(ctx, "msdl:telemetry:sentinel_count:"+day).Int64() + distinct, _ := rdb.PFCount(ctx, "msdl:telemetry:sentinel_hll:"+day).Result() + return map[string]interface{}{ + "date": day, + "errors_today": count, + "distinct_sources_est": distinct, + } +} + func warmEvalCache() { for slug, product := range evalProductMap { go func(s string, p EvalProduct) { -- 2.45.3 From 327c5e195dfc9d77ee89e1fee0ad5b8812e92588 Mon Sep 17 00:00:00 2001 From: Shekhar Vaidya Date: Mon, 13 Jul 2026 12:43:31 +0530 Subject: [PATCH 4/4] simplify(backend): drop date-scoping for Sentinel stats, use flat all-time fields Matches the shape actually asked for and how every other telemetry counter in this file already works (all-time cumulative via HIncrBy, no expiry, no per-day key rotation): /metrics now exposes flat sentinel_errors and sentinel_distinct_sources_est fields instead of a nested, date-scoped sentinel_today object. Co-Authored-By: Claude Sonnet 4.6 --- backend/main.go | 44 +++++++++++++++++--------------------------- 1 file changed, 17 insertions(+), 27 deletions(-) diff --git a/backend/main.go b/backend/main.go index e4a7b7c..9df52bb 100644 --- a/backend/main.go +++ b/backend/main.go @@ -937,7 +937,6 @@ func fetchDownloadLinksFromMS(productID, skuID string) ([]byte, error) { jar = &simpleCookieJar{} } - proxyQ := url.Values{} proxyQ.Set("profile", PROFILE) proxyQ.Set("productEditionId", "undefined") @@ -1676,18 +1675,13 @@ func handleTelemetry(w http.ResponseWriter, r *http.Request) { if !p.Success && p.Error != "" && len(p.Error) <= 200 { rdb.HIncrBy(ctx, "msdl:telemetry:errors", p.Error, 1) if strings.Contains(p.Error, "Sentinel") { - // Track today's Sentinel-rejection count and an approximate + // All-time Sentinel-rejection count, plus an approximate // distinct-source count (via HyperLogLog on the reporting // IP) so /metrics can show whether rejections are // concentrated in a few sources or spread across many, // without ever storing raw IPs. - day := time.Now().UTC().Format("2006-01-02") - countKey := "msdl:telemetry:sentinel_count:" + day - hllKey := "msdl:telemetry:sentinel_hll:" + day - rdb.Incr(ctx, countKey) - rdb.Expire(ctx, countKey, 48*time.Hour) - rdb.PFAdd(ctx, hllKey, ip) - rdb.Expire(ctx, hllKey, 48*time.Hour) + rdb.Incr(ctx, "msdl:telemetry:sentinel_count") + rdb.PFAdd(ctx, "msdl:telemetry:sentinel_hll", ip) } } } @@ -1789,7 +1783,7 @@ func handleMetrics(w http.ResponseWriter, r *http.Request) { evalCacheMu.RUnlock() telemetry := loadTelemetryFromRedis() - sentinelToday := loadSentinelTodayFromRedis() + sentinelErrors, sentinelDistinctSources := loadSentinelStatsFromRedis() w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ @@ -1817,30 +1811,26 @@ func handleMetrics(w http.ResponseWriter, r *http.Request) { "hit_rate": hitRate(evalHits, evalReqs), "cache_size": evalSize, }, - "neg_cache_size": negSize, - "total_ms_fetches": skuFetches + linkFetches, - "telemetry": telemetry, - "sentinel_today": sentinelToday, + "neg_cache_size": negSize, + "total_ms_fetches": skuFetches + linkFetches, + "telemetry": telemetry, + "sentinel_errors": sentinelErrors, + "sentinel_distinct_sources_est": sentinelDistinctSources, }) } -// loadSentinelTodayFromRedis returns today's Sentinel-rejection count and an -// approximate distinct-source count (from the HyperLogLog seeded in -// handleTelemetry). Returns nil when Redis is unavailable. -func loadSentinelTodayFromRedis() map[string]interface{} { +// loadSentinelStatsFromRedis returns the all-time Sentinel-rejection count +// and an approximate distinct-source count (from the HyperLogLog seeded in +// handleTelemetry). Returns (0, 0) when Redis is unavailable. +func loadSentinelStatsFromRedis() (int64, int64) { if rdb == nil { - return nil + return 0, 0 } ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) defer cancel() - day := time.Now().UTC().Format("2006-01-02") - count, _ := rdb.Get(ctx, "msdl:telemetry:sentinel_count:"+day).Int64() - distinct, _ := rdb.PFCount(ctx, "msdl:telemetry:sentinel_hll:"+day).Result() - return map[string]interface{}{ - "date": day, - "errors_today": count, - "distinct_sources_est": distinct, - } + count, _ := rdb.Get(ctx, "msdl:telemetry:sentinel_count").Int64() + distinct, _ := rdb.PFCount(ctx, "msdl:telemetry:sentinel_hll").Result() + return count, distinct } func warmEvalCache() { -- 2.45.3