internal/counter: preserve programs' prerelease versions

Previously, any versions including "-" were treated like dev versions.
But program owners may want to check with telemetry, whether a sufficient
number of users had tested prerelease versions before making them
official releases. We still don't want to count every pseudo version
individually, so continue to map pseudo versions to 'devel'.

Fixes golang/go#63619

Change-Id: Ia4186560ce8987353b6af390dead10108bf0758e
Reviewed-on: https://go-review.googlesource.com/c/telemetry/+/547878
Commit-Queue: Hyang-Ah Hana Kim <hyangah@gmail.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Robert Findley <rfindley@google.com>
Auto-Submit: Hyang-Ah Hana Kim <hyangah@gmail.com>
diff --git a/internal/counter/file.go b/internal/counter/file.go
index aef6309..1fac726 100644
--- a/internal/counter/file.go
+++ b/internal/counter/file.go
@@ -21,6 +21,7 @@
 	"time"
 	"unsafe"
 
+	"golang.org/x/mod/module"
 	"golang.org/x/telemetry/internal/mmap"
 	"golang.org/x/telemetry/internal/telemetry"
 )
@@ -136,19 +137,7 @@
 		return
 	}
 
-	goVers := info.GoVersion
-	if strings.Contains(goVers, "devel") || strings.Contains(goVers, "-") {
-		goVers = "devel"
-	}
-	progPkgPath := info.Path
-	if progPkgPath == "" {
-		progPkgPath = strings.TrimSuffix(filepath.Base(os.Args[0]), ".exe")
-	}
-	prog := path.Base(progPkgPath)
-	progVers := info.Main.Version
-	if strings.Contains(progVers, "devel") || strings.Contains(progVers, "-") {
-		progVers = "devel"
-	}
+	goVers, progPkgPath, prog, progVers := programInfo(info)
 	f.meta = fmt.Sprintf("TimeBegin: %s\nTimeEnd: %s\nProgram: %s\nVersion: %s\nGoVersion: %s\nGOOS: %s\nGOARCH: %s\n\n",
 		begin.Format(time.RFC3339), end.Format(time.RFC3339),
 		progPkgPath, progVers, goVers, runtime.GOOS, runtime.GOARCH)
@@ -163,6 +152,24 @@
 	f.namePrefix = filepath.Join(dir, prefix)
 }
 
+func programInfo(info *debug.BuildInfo) (goVers, progPkgPath, prog, progVers string) {
+	goVers = info.GoVersion
+	if strings.Contains(goVers, "devel") || strings.Contains(goVers, "-") {
+		goVers = "devel"
+	}
+	progPkgPath = info.Path
+	if progPkgPath == "" {
+		progPkgPath = strings.TrimSuffix(filepath.Base(os.Args[0]), ".exe")
+	}
+	prog = path.Base(progPkgPath)
+	progVers = info.Main.Version
+	if strings.Contains(progVers, "devel") || module.IsPseudoVersion(progVers) {
+		// we don't want to track pseudo versions, but may want to track prereleases.
+		progVers = "devel"
+	}
+	return goVers, progPkgPath, prog, progVers
+}
+
 // filename returns the name of the file to use for f,
 // given the current time now.
 // It also returns the time when that name will no longer be valid
diff --git a/internal/counter/file_test.go b/internal/counter/file_test.go
new file mode 100644
index 0000000..e1ac90f
--- /dev/null
+++ b/internal/counter/file_test.go
@@ -0,0 +1,57 @@
+// Copyright 2023 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package counter
+
+import (
+	"runtime/debug"
+	"testing"
+)
+
+func TestProgramInfo_ProgramVersion(t *testing.T) {
+	tests := []struct {
+		name string
+		in   string
+		want string
+	}{
+		{
+			name: "(devel)",
+			in:   "(devel)",
+			want: "devel",
+		},
+		{
+			name: "empty version",
+			in:   "",
+			want: "",
+		},
+		{
+			name: "prerelease",
+			in:   "v0.14.0-pre.1",
+			want: "v0.14.0-pre.1",
+		},
+		{
+			name: "pseudoversion",
+			in:   "v0.0.0-20231207172801-3c8b0df0c3fd",
+			want: "devel",
+		},
+	}
+	type info struct {
+		GoVers, ProgPkgPath, Prog, ProgVer string
+	}
+	buildInfo, ok := debug.ReadBuildInfo()
+	if !ok {
+		t.Fatal("cannot use debug.ReadBuildInfo")
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			in := *buildInfo
+			in.Main.Version = tt.in
+			_, _, _, got := programInfo(&in)
+			if got != tt.want {
+				t.Errorf("program version = %q, want %q", got, tt.want)
+			}
+		})
+	}
+}