feat(insights): report the app store or hosting platform via ND_PLATFORM (#5956)

* feat(insights): report the app store or hosting platform via ND_PLATFORM

Insights had no way to tell where an instance is deployed. The existing `os.package` field is written only by our own packagers and holds just `deb`, `rpm` or `msi`, so it answers "which installer", not "which platform". Overloading it would mix two unrelated dimensions in the same field.

This adds a separate top-level `platform` field, self-declared by the deployer through the `ND_PLATFORM` environment variable. App stores and hosting providers (ZimaOS, PikaPods, TrueNAS, Unraid, and others) generally deploy our container image unmodified and can only inject environment variables, so an env var is the one marker they can all set. It is deliberately not a config option: it is a packager marker, not something users should tune, and it stays out of the config surface.

Both values are now whitespace-trimmed. The msi packager writes the file with `echo`, so `os.package` has been arriving as `"msi\n"` and sorting separately from `"msi"` in any aggregation.

* test(insights): isolate hostingPlatform specs from an inherited ND_PLATFORM

The spec asserting an empty result read the real environment, so it failed on any machine that already had ND_PLATFORM set. Unset it per-spec, using Setenv first so Ginkgo restores the original value on cleanup.
This commit is contained in:
Deluan Quintão 2026-08-14 13:46:52 -04:00 committed by GitHub
parent 95615bcb18
commit b617a878b9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 104 additions and 6 deletions

View File

@ -10,6 +10,7 @@ import (
"path/filepath"
"runtime"
"runtime/debug"
"strings"
"sync"
"sync/atomic"
"time"
@ -153,6 +154,17 @@ func getFSInfo(path string) *insights.FSInfo {
return &info
}
// installedPackage returns the official installer format used, as written by our own packagers.
func installedPackage() string {
data, _ := os.ReadFile(filepath.Join(conf.Server.DataFolder.String(), ".package"))
return strings.TrimSpace(string(data))
}
// hostingPlatform is env-based, not a file, as app stores can only inject env vars into our image.
func hostingPlatform() string {
return strings.TrimSpace(os.Getenv("ND_PLATFORM"))
}
var staticData = sync.OnceValue(func() insights.Data {
// Basic info
data := insights.Data{
@ -165,11 +177,8 @@ var staticData = sync.OnceValue(func() insights.Data {
data.OS.Containerized = consts.InContainer
// Install info
packageFilename := filepath.Join(conf.Server.DataFolder.String(), ".package")
packageFileData, err := os.ReadFile(packageFilename)
if err == nil {
data.OS.Package = string(packageFileData)
}
data.OS.Package = installedPackage()
data.Platform = hostingPlatform()
// OS info
data.OS.Type = runtime.GOOS

View File

@ -4,7 +4,9 @@ type Data struct {
InsightsID string `json:"id"`
Version string `json:"version"`
Uptime int64 `json:"uptime"`
Build struct {
// Platform is the app store or hosting provider this instance runs on, self-declared via ND_PLATFORM
Platform string `json:"platform,omitempty"`
Build struct {
// build settings used by the Go compiler
Settings map[string]string `json:"settings"`
GoVersion string `json:"goVersion"`

View File

@ -0,0 +1,70 @@
package metrics
import (
"os"
"path/filepath"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/conf/configtest"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("installedPackage", func() {
BeforeEach(func() {
DeferCleanup(configtest.SetupConfig())
conf.Server.DataFolder = conf.NewDir(GinkgoT().TempDir())
})
It("returns empty when there's no .package file", func() {
Expect(installedPackage()).To(BeEmpty())
})
It("reads the .package file from the data folder", func() {
writePackageFile("deb")
Expect(installedPackage()).To(Equal("deb"))
})
It("trims surrounding whitespace, as the msi packager writes a trailing newline", func() {
writePackageFile("msi\n")
Expect(installedPackage()).To(Equal("msi"))
})
It("ignores ND_PLATFORM", func() {
GinkgoT().Setenv("ND_PLATFORM", "zimaos")
Expect(installedPackage()).To(BeEmpty())
})
})
var _ = Describe("hostingPlatform", func() {
BeforeEach(func() {
// Setenv registers the restore, then unset so an inherited value can't leak in
GinkgoT().Setenv("ND_PLATFORM", "")
Expect(os.Unsetenv("ND_PLATFORM")).To(Succeed())
})
It("returns empty when ND_PLATFORM is not set", func() {
Expect(hostingPlatform()).To(BeEmpty())
})
It("reads ND_PLATFORM", func() {
GinkgoT().Setenv("ND_PLATFORM", "zimaos")
Expect(hostingPlatform()).To(Equal("zimaos"))
})
It("trims surrounding whitespace", func() {
GinkgoT().Setenv("ND_PLATFORM", " pikapods\n")
Expect(hostingPlatform()).To(Equal("pikapods"))
})
})
func writePackageFile(content string) {
GinkgoHelper()
path := filepath.Join(conf.Server.DataFolder.String(), ".package")
Expect(os.WriteFile(path, []byte(content), 0600)).To(Succeed())
}

View File

@ -0,0 +1,17 @@
package metrics
import (
"testing"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/tests"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
func TestMetrics(t *testing.T) {
tests.Init(t, false)
log.SetLevel(log.LevelFatal)
RegisterFailHandler(Fail)
RunSpecs(t, "Metrics Suite")
}