diff --git a/changelog.md b/changelog.md index 475307318d..348ba163d5 100644 --- a/changelog.md +++ b/changelog.md @@ -2,6 +2,26 @@ ## Unreleased +### Features + +- [#3707](https://github.com/ignite/cli/pull/3707) and [#4094](https://github.com/ignite/cli/pull/4094) Add collections support. +- [#3977](https://github.com/ignite/cli/pull/3977) Add `chain lint` command to lint the chain's codebase using `golangci-lint` +- [#3770](https://github.com/ignite/cli/pull/3770) Add `scaffold configs` and `scaffold params` commands +- [#4001](https://github.com/ignite/cli/pull/4001) Improve `xgenny` dry run +- [#3967](https://github.com/ignite/cli/issues/3967) Add HD wallet parameters `address index` and `account number` to the chain account config +- [#4004](https://github.com/ignite/cli/pull/4004) Remove all import placeholders using the `xast` pkg +- [#4076](https://github.com/ignite/cli/pull/4076) Remove the ignite `relayer` and `tools` commands with all ts-relayer logic +- [#4071](https://github.com/ignite/cli/pull/4071) Support custom proto path +- [#3718](https://github.com/ignite/cli/pull/3718) Add `gen-mig-diffs` tool app to compare scaffold output of two versions of ignite +- [#4077](https://github.com/ignite/cli/pull/4077) Merge the swagger files manually instead use nodetime `swagger-combine` +- [#4090](https://github.com/ignite/cli/pull/4090) Remove `protoc` pkg and also nodetime helpers `ts-proto` and `sta` +- [#4100](https://github.com/ignite/cli/pull/4100) Set the `proto-dir` flag only for the `scaffold chain` command and use the proto path from the config +- [#4111](https://github.com/ignite/cli/pull/4111) Remove vuex generation +- [#4133](https://github.com/ignite/cli/pull/4133) Improve buf rate limit +- [#4113](https://github.com/ignite/cli/pull/4113) Generate chain config documentation automatically +- [#4131](https://github.com/ignite/cli/pull/4131) Support `bytes` as data type in the `scaffold` commands +- [#4095](https://github.com/ignite/cli/pull/4095) Migrate to matomo analytics + ### Changes - [#4149](https://github.com/ignite/cli/pull/4149) Bump cometbft to `v0.38.7` diff --git a/ignite/cmd/version.go b/ignite/cmd/version.go index f61136bd58..f353255ed9 100644 --- a/ignite/cmd/version.go +++ b/ignite/cmd/version.go @@ -11,8 +11,13 @@ func NewVersion() *cobra.Command { c := &cobra.Command{ Use: "version", Short: "Print the current build information", - Run: func(cmd *cobra.Command, _ []string) { - cmd.Println(version.Long(cmd.Context())) + RunE: func(cmd *cobra.Command, _ []string) error { + v, err := version.Long(cmd.Context()) + if err != nil { + return err + } + cmd.Println(v) + return nil }, } return c diff --git a/ignite/internal/analytics/analytics.go b/ignite/internal/analytics/analytics.go index 018748eb1c..b6bef3813f 100644 --- a/ignite/internal/analytics/analytics.go +++ b/ignite/internal/analytics/analytics.go @@ -1,10 +1,10 @@ package analytics import ( + "context" "encoding/json" "os" "path/filepath" - "runtime" "strconv" "strings" "sync" @@ -12,14 +12,22 @@ import ( "github.com/manifoldco/promptui" "github.com/spf13/cobra" +<<<<<<< HEAD "github.com/ignite/cli/v28/ignite/pkg/gacli" "github.com/ignite/cli/v28/ignite/pkg/gitpod" "github.com/ignite/cli/v28/ignite/pkg/randstr" "github.com/ignite/cli/v28/ignite/version" +======= + "github.com/ignite/cli/v29/ignite/config" + "github.com/ignite/cli/v29/ignite/pkg/gitpod" + "github.com/ignite/cli/v29/ignite/pkg/matomo" + "github.com/ignite/cli/v29/ignite/pkg/randstr" + "github.com/ignite/cli/v29/ignite/version" +>>>>>>> e49e11df (feat: migrate to matomo analytics (#4095)) ) const ( - telemetryEndpoint = "https://telemetry-cli.ignite.com" + telemetryEndpoint = "https://matomo-cli.ignite.com" envDoNotTrack = "DO_NOT_TRACK" envCI = "CI" envGitHubActions = "GITHUB_ACTIONS" @@ -27,7 +35,7 @@ const ( igniteAnonIdentity = "anon_identity.json" ) -var gaclient gacli.Client +var matomoClient matomo.Client // anonIdentity represents an analytics identity file. type anonIdentity struct { @@ -38,7 +46,11 @@ type anonIdentity struct { } func init() { - gaclient = gacli.New(telemetryEndpoint) + matomoClient = matomo.New( + telemetryEndpoint, + matomo.WithIDSite(4), + matomo.WithSource("https://cli.ignite.com"), + ) } // SendMetric send command metrics to analytics. @@ -52,23 +64,46 @@ func SendMetric(wg *sync.WaitGroup, cmd *cobra.Command) { return } - path := cmd.CommandPath() - met := gacli.Metric{ - Name: cmd.Name(), - Cmd: path, - Tag: strings.ReplaceAll(path, " ", "+"), - OS: runtime.GOOS, - Arch: runtime.GOARCH, - SessionID: dntInfo.Name, - Version: version.Version, - IsGitPod: gitpod.IsOnGitpod(), - IsCI: getIsCI(), + versionInfo, err := version.GetInfo(context.Background()) + if err != nil { + return + } + + var ( + path = cmd.CommandPath() + scaffoldType = "" + ) + if strings.Contains(path, "ignite scaffold") { + splitCMD := strings.Split(path, " ") + if len(splitCMD) > 2 { + scaffoldType = splitCMD[2] + } + } + + met := matomo.Metric{ + Name: cmd.Name(), + Cmd: path, + ScaffoldType: scaffoldType, + OS: versionInfo.OS, + Arch: versionInfo.Arch, + Version: versionInfo.CLIVersion, + CLIVersion: versionInfo.CLIVersion, + GoVersion: versionInfo.GoVersion, + SDKVersion: versionInfo.SDKVersion, + BuildDate: versionInfo.BuildDate, + SourceHash: versionInfo.SourceHash, + ConfigVersion: versionInfo.ConfigVersion, + Uname: versionInfo.Uname, + CWD: versionInfo.CWD, + BuildFromSource: versionInfo.BuildFromSource, + IsGitPod: gitpod.IsOnGitpod(), + IsCI: getIsCI(), } wg.Add(1) go func() { defer wg.Done() - _ = gaclient.SendMetric(met) + _ = matomoClient.SendMetric(dntInfo.Name, met) }() } @@ -97,7 +132,7 @@ func checkDNT() (anonIdentity, error) { return i, nil } - i.Name = randstr.Runes(10) + i.Name = randstr.Runes(16) i.DoNotTrack = false prompt := promptui.Select{ diff --git a/ignite/pkg/gacli/doc.go b/ignite/pkg/gacli/doc.go deleted file mode 100644 index 9b905a5d3a..0000000000 --- a/ignite/pkg/gacli/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package gacli is a client for Google Analytics to send data points for hint-type=event. -package gacli diff --git a/ignite/pkg/matomo/matomo.go b/ignite/pkg/matomo/matomo.go new file mode 100644 index 0000000000..4c05e5967d --- /dev/null +++ b/ignite/pkg/matomo/matomo.go @@ -0,0 +1,266 @@ +// Package matomo is a client for Matomo to send data points for hint-type=event. +package matomo + +import ( + "fmt" + "math/rand" + "net/http" + "net/url" + "strings" + "time" + + "github.com/google/go-querystring/query" + + "github.com/ignite/cli/v29/ignite/pkg/errors" +) + +type ( + // Client is an analytics client. + Client struct { + endpoint string + idSite uint // Matomo ID Site. + tokenAuth string // Matomo Token Auth. + source string + httpClient http.Client + } + + // Params analytics metrics body. + Params struct { + IDSite uint `url:"idsite"` + Rec uint `url:"rec"` + ActionName string `url:"action_name"` + APIVersion uint `url:"apiv"` + TokenAuth string `url:"token_auth,omitempty"` + Rand uint64 `url:"rand,omitempty"` + URL string `url:"url,omitempty"` + UTMSource string `url:"utm_source,omitempty"` + UTMMedium string `url:"utm_medium,omitempty"` + UTMCampaign string `url:"utm_campaign,omitempty"` + UTMContent string `url:"utm_content,omitempty"` + UserID string `url:"uid,omitempty"` + UserAgent string `url:"ua,omitempty"` + Hour int `url:"h,omitempty"` + Minute int `url:"m,omitempty"` + Second int `url:"s,omitempty"` + + // Dimension1 development mode boolean. + // 1 = devMode ON | 0 = devMode OFF. + Dimension1 uint `url:"dimension1"` + + // Dimension2 internal boolean. + // 1 = internal ON not supported at present | 0 = internal OFF. + Dimension2 uint `url:"dimension2"` + + // Dimension3 is gitpod (0 or 1). + // 1 = isGitpod ON | 0 = isGitpod OFF. + Dimension3 uint `url:"dimension3"` + + // Dimension4 ignite version + Dimension4 string `url:"dimension4,omitempty"` + + // Dimension6 ignite config version + Dimension6 string `url:"dimension6,omitempty"` + + // Dimension7 full cli command + Dimension7 string `url:"dimension7,omitempty"` + + // Dimension11 scaffold customization type + Dimension11 string `url:"dimension11,omitempty"` + + // Dimension13 command level 1. + Dimension13 string `url:"dimension13,omitempty"` + + // Dimension14 command level 2. + Dimension14 string `url:"dimension14,omitempty"` + + // Dimension15 command level 3. + Dimension15 string `url:"dimension15,omitempty"` + + // Dimension16 command level 4. + Dimension16 string `url:"dimension16,omitempty"` + + // Dimension17 cosmos-sdk version. + Dimension17 string `url:"dimension17,omitempty"` + + // Dimension18 operational system. + Dimension18 string `url:"dimension18,omitempty"` + + // Dimension19 system architecture. + Dimension19 string `url:"dimension19,omitempty"` + + // Dimension20 golang version. + Dimension20 string `url:"dimension20,omitempty"` + + // Dimension21 command level 5. + Dimension21 string `url:"dimension21,omitempty"` + + // Dimension22 command level 6. + Dimension22 string `url:"dimension22,omitempty"` + } + // Metric represents a custom data. + Metric struct { + Name string + Cmd string + OS string + Arch string + Version string + CLIVersion string + GoVersion string + SDKVersion string + BuildDate string + SourceHash string + ConfigVersion string + Uname string + CWD string + ScaffoldType string + BuildFromSource bool + IsGitPod bool + IsCI bool + } +) + +// Option configures code generation. +type Option func(*Client) + +// WithIDSite adds an id site. +func WithIDSite(idSite uint) Option { + return func(c *Client) { + c.idSite = idSite + } +} + +// WithTokenAuth adds a matomo token authentication. +func WithTokenAuth(tokenAuth string) Option { + return func(c *Client) { + c.tokenAuth = tokenAuth + } +} + +// WithSource adds a matomo URL source. +func WithSource(source string) Option { + return func(c *Client) { + c.source = source + } +} + +// New creates a new Matomo client. +func New(endpoint string, opts ...Option) Client { + c := Client{ + endpoint: endpoint, + source: endpoint, + httpClient: http.Client{ + Timeout: 1500 * time.Millisecond, + }, + } + // apply analytics options. + for _, o := range opts { + o(&c) + } + return c +} + +// Send sends metric event to analytics. +func (c Client) Send(params Params) error { + requestURL, err := url.Parse(c.endpoint) + if err != nil { + return err + } + + // encode request parameters. + queryParams, err := query.Values(params) + if err != nil { + return err + } + requestURL.RawQuery = queryParams.Encode() + + // Create an HTTP request with the payload. + resp, err := c.httpClient.Get(requestURL.String()) + if err != nil { + return errors.Wrapf(err, "error creating HTTP request: %s", requestURL.String()) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return errors.Errorf("error to add matomo analytics metric. Status code: %d", resp.StatusCode) + } + + return nil +} + +// SendMetric build the metrics and send to analytics. +func (c Client) SendMetric(sessionID string, metric Metric) error { + var ( + now = time.Now() + r = rand.New(rand.NewSource(now.Unix())) + utmMedium = "dev" + ) + if !metric.BuildFromSource { + utmMedium = "binary" + } + + cmd := splitCommand(metric.Cmd) + + return c.Send(Params{ + IDSite: c.idSite, + Rec: 1, + APIVersion: 1, + TokenAuth: c.tokenAuth, + Rand: r.Uint64(), + URL: c.metricURL(metric.Cmd), + UTMSource: "source-code-github", + UTMMedium: utmMedium, + UTMCampaign: metric.CLIVersion, + UTMContent: fmt.Sprintf("commit-%s", metric.SourceHash), + UserID: sessionID, + UserAgent: "Go-http-client", + ActionName: metric.Cmd, + Hour: now.Hour(), + Minute: now.Minute(), + Second: now.Second(), + Dimension1: 0, + Dimension2: formatBool(metric.IsCI), + Dimension3: formatBool(metric.IsGitPod), + Dimension4: metric.Version, + Dimension6: metric.ConfigVersion, + Dimension7: metric.Cmd, + Dimension11: metric.ScaffoldType, + Dimension13: cmd[0], + Dimension14: cmd[1], + Dimension15: cmd[2], + Dimension16: cmd[3], + Dimension17: metric.SDKVersion, + Dimension18: metric.OS, + Dimension19: metric.Arch, + Dimension20: metric.GoVersion, + Dimension21: cmd[4], + Dimension22: cmd[5], + }) +} + +// formatBool returns "1" or "0" according to the value of b. +func formatBool(b bool) uint { + if b { + return 1 + } + return 0 +} + +// splitCommand splice the command into a slice with length 6. +func splitCommand(cmd string) []string { + var ( + splitCmd = strings.Split(cmd, " ") + cmdLevels = make([]string, 6) + ) + for i := 0; i < len(cmdLevels); i++ { + if i >= len(splitCmd) { + break + } + cmdLevels[i] = splitCmd[i] + } + return cmdLevels +} + +// metricURL build the metric URL. +func (c Client) metricURL(cmd string) string { + return fmt.Sprintf("%s/%s", c.source, strings.ReplaceAll(cmd, " ", "_")) +} diff --git a/ignite/version/version.go b/ignite/version/version.go index 65c6cc85ed..b35eca30fc 100644 --- a/ignite/version/version.go +++ b/ignite/version/version.go @@ -7,6 +7,7 @@ import ( "os" "runtime" "runtime/debug" + "strconv" "strings" "text/tabwriter" @@ -34,6 +35,21 @@ Please, follow the migration guide to upgrade your chain to the latest version a // Version is the semantic version of Ignite CLI. var Version = versionDev +type Info struct { + CLIVersion string + GoVersion string + SDKVersion string + BuildDate string + SourceHash string + ConfigVersion string + OS string + Arch string + Uname string + CWD string + IsGitpod bool + BuildFromSource bool +} + // CheckNext checks whether there is a new version of Ignite CLI. func CheckNext(ctx context.Context) (isAvailable bool, version string, err error) { if Version == versionDev || Version == versionNightly { @@ -76,6 +92,11 @@ func getLatestReleaseTag(ctx context.Context) (string, error) { return *latest.TagName, nil } +// fromSource check if the binary was build from source using the CLI version. +func fromSource() bool { + return Version == versionDev +} + // resolveDevVersion creates a string for version printing if the version being used is "development". // the version will be of the form "LATEST-dev" where LATEST is the latest tagged release. func resolveDevVersion(ctx context.Context) string { @@ -100,24 +121,65 @@ func resolveDevVersion(ctx context.Context) string { } // Long generates a detailed version info. -func Long(ctx context.Context) string { +func Long(ctx context.Context) (string, error) { var ( - w = &tabwriter.Writer{} - b = &bytes.Buffer{} + w = &tabwriter.Writer{} + b = &bytes.Buffer{} + ) + + info, err := GetInfo(ctx) + if err != nil { + return "", err + } + + write := func(k, v string) { + fmt.Fprintf(w, "%s:\t%s\n", k, v) + } + w.Init(b, 0, 8, 0, '\t', 0) + + write("Ignite CLI version", info.CLIVersion) + write("Ignite CLI build date", info.BuildDate) + write("Ignite CLI source hash", info.SourceHash) + write("Ignite CLI config version", info.ConfigVersion) + write("Cosmos SDK version", info.SDKVersion) + + write("Your OS", info.OS) + write("Your arch", info.Arch) + write("Your go version", info.GoVersion) + write("Your uname -a", info.Uname) + + if info.CWD != "" { + write("Your cwd", info.CWD) + } + + write("Is on Gitpod", strconv.FormatBool(info.IsGitpod)) + + if err := w.Flush(); err != nil { + return "", err + } + + return b.String(), nil +} + +// GetInfo gets the CLI info. +func GetInfo(ctx context.Context) (Info, error) { + var ( + info Info + modified bool + date = "undefined" head = "undefined" - modified bool sdkVersion = "undefined" ) - if info, ok := debug.ReadBuildInfo(); ok { - for _, dep := range info.Deps { + if buildInfo, ok := debug.ReadBuildInfo(); ok { + for _, dep := range buildInfo.Deps { if cosmosver.CosmosSDKModulePathPattern.MatchString(dep.Path) { sdkVersion = dep.Version break } } - for _, kv := range info.Settings { + for _, kv := range buildInfo.Settings { switch kv.Key { case "vcs.revision": head = kv.Value @@ -133,59 +195,41 @@ func Long(ctx context.Context) string { } } - write := func(k string, v interface{}) { - fmt.Fprintf(w, "%s:\t%v\n", k, v) + goVersionBuf := &bytes.Buffer{} + if err := exec.Exec(ctx, []string{"go", "version"}, exec.StepOption(step.Stdout(goVersionBuf))); err != nil { + return info, err } - w.Init(b, 0, 8, 0, '\t', 0) - - write("Ignite CLI version", resolveDevVersion(ctx)) - write("Ignite CLI build date", date) - write("Ignite CLI source hash", head) - write("Ignite CLI config version", chainconfig.LatestVersion) - write("Cosmos SDK version", sdkVersion) - - write("Your OS", runtime.GOOS) - write("Your arch", runtime.GOARCH) - - cmdOut := &bytes.Buffer{} - - nodeJSCmd := "node" - if xexec.IsCommandAvailable(nodeJSCmd) { - cmdOut.Reset() - - err := exec.Exec(ctx, []string{nodeJSCmd, "-v"}, exec.StepOption(step.Stdout(cmdOut))) - if err == nil { - write("Your Node.js version", strings.TrimSpace(cmdOut.String())) - } - } - - cmdOut.Reset() - err := exec.Exec(ctx, []string{"go", "version"}, exec.StepOption(step.Stdout(cmdOut))) - if err != nil { - panic(err) - } - write("Your go version", strings.TrimSpace(cmdOut.String())) - - unameCmd := "uname" + var ( + unameCmd = "uname" + uname = "" + ) if xexec.IsCommandAvailable(unameCmd) { - cmdOut.Reset() - - err := exec.Exec(ctx, []string{unameCmd, "-a"}, exec.StepOption(step.Stdout(cmdOut))) - if err == nil { - write("Your uname -a", strings.TrimSpace(cmdOut.String())) + unameBuf := &bytes.Buffer{} + unameBuf.Reset() + if err := exec.Exec(ctx, []string{unameCmd, "-a"}, exec.StepOption(step.Stdout(unameBuf))); err != nil { + return info, err } + uname = strings.TrimSpace(unameBuf.String()) } + info.Uname = uname + info.CLIVersion = resolveDevVersion(ctx) + info.BuildDate = date + info.SourceHash = head + info.ConfigVersion = fmt.Sprintf("v%d", chainconfig.LatestVersion) + info.SDKVersion = sdkVersion + info.OS = runtime.GOOS + info.Arch = runtime.GOARCH + info.GoVersion = strings.TrimSpace(goVersionBuf.String()) + info.IsGitpod = gitpod.IsOnGitpod() + info.BuildFromSource = fromSource() + if cwd, err := os.Getwd(); err == nil { - write("Your cwd", cwd) + info.CWD = cwd } - write("Is on Gitpod", gitpod.IsOnGitpod()) - - w.Flush() - - return b.String() + return info, nil } // AssertSupportedCosmosSDKVersion asserts that a Cosmos SDK version is supported by Ignite CLI.