@automattic/vip 4.1.4 → 4.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/cmd/vip-next/commands/devenv.go +1 -0
  2. package/cmd/vip-next/commands/devenv_envvar.go +6 -0
  3. package/cmd/vip-next/commands/devenv_lifecycle.go +7 -0
  4. package/cmd/vip-next/commands/devenv_resolve.go +15 -3
  5. package/cmd/vip-next/commands/devenv_telemetry.go +265 -0
  6. package/cmd/vip-next/commands/devenv_telemetry_test.go +322 -0
  7. package/dist/lib/dev-environment/dev-environment-cli.js +8 -5
  8. package/node_modules/@apollo/client/CHANGELOG.md +6 -0
  9. package/node_modules/@apollo/client/__cjs/react/hooks/useLazyQuery.cjs +27 -1
  10. package/node_modules/@apollo/client/__cjs/react/hooks/useLazyQuery.cjs.map +1 -1
  11. package/node_modules/@apollo/client/__cjs/version.cjs +1 -1
  12. package/node_modules/@apollo/client/package.json +1 -1
  13. package/node_modules/@apollo/client/react/hooks/useLazyQuery.js +27 -1
  14. package/node_modules/@apollo/client/react/hooks/useLazyQuery.js.map +1 -1
  15. package/node_modules/@apollo/client/react/hooks-compiled/useLazyQuery.js +27 -1
  16. package/node_modules/@apollo/client/react/hooks-compiled/useLazyQuery.js.map +1 -1
  17. package/node_modules/@apollo/client/version.js +1 -1
  18. package/node_modules/@types/node/README.md +2 -2
  19. package/node_modules/@types/node/child_process.d.ts +1 -1
  20. package/node_modules/@types/node/crypto.d.ts +20 -8
  21. package/node_modules/@types/node/ffi.d.ts +14 -0
  22. package/node_modules/@types/node/http2.d.ts +3 -2
  23. package/node_modules/@types/node/net.d.ts +16 -4
  24. package/node_modules/@types/node/package.json +2 -17
  25. package/node_modules/@types/node/process.d.ts +1 -0
  26. package/node_modules/@types/node/quic.d.ts +39 -1
  27. package/node_modules/@types/node/stream/iter.d.ts +9 -12
  28. package/node_modules/@types/node/test/reporters.d.ts +1 -0
  29. package/node_modules/@types/node/test.d.ts +72 -0
  30. package/node_modules/@types/node/tls.d.ts +14 -10
  31. package/node_modules/@types/node/vm.d.ts +2 -1
  32. package/node_modules/@types/node/worker_threads.d.ts +4 -1
  33. package/node_modules/socket.io-client/README.md +1 -0
  34. package/node_modules/socket.io-client/build/cjs/index.d.ts +3 -3
  35. package/node_modules/socket.io-client/build/cjs/manager.d.ts +1 -2
  36. package/node_modules/socket.io-client/build/cjs/manager.js +17 -7
  37. package/node_modules/socket.io-client/build/cjs/socket.d.ts +5 -2
  38. package/node_modules/socket.io-client/build/cjs/socket.js +0 -1
  39. package/node_modules/socket.io-client/build/esm/index.d.ts +3 -3
  40. package/node_modules/socket.io-client/build/esm/index.js +1 -1
  41. package/node_modules/socket.io-client/build/esm/manager.d.ts +1 -2
  42. package/node_modules/socket.io-client/build/esm/package.json +1 -1
  43. package/node_modules/socket.io-client/build/esm/socket.d.ts +5 -2
  44. package/node_modules/socket.io-client/build/esm/socket.js +0 -1
  45. package/node_modules/socket.io-client/build/esm-debug/index.d.ts +3 -3
  46. package/node_modules/socket.io-client/build/esm-debug/index.js +1 -1
  47. package/node_modules/socket.io-client/build/esm-debug/manager.d.ts +1 -2
  48. package/node_modules/socket.io-client/build/esm-debug/package.json +1 -1
  49. package/node_modules/socket.io-client/build/esm-debug/socket.d.ts +5 -2
  50. package/node_modules/socket.io-client/build/esm-debug/socket.js +0 -1
  51. package/node_modules/socket.io-client/dist/socket.io.esm.min.js +3 -3
  52. package/node_modules/socket.io-client/dist/socket.io.esm.min.js.map +1 -1
  53. package/node_modules/socket.io-client/dist/socket.io.js +174 -242
  54. package/node_modules/socket.io-client/dist/socket.io.js.map +1 -1
  55. package/node_modules/socket.io-client/dist/socket.io.min.js +3 -3
  56. package/node_modules/socket.io-client/dist/socket.io.min.js.map +1 -1
  57. package/node_modules/socket.io-client/dist/socket.io.msgpack.min.js +3 -3
  58. package/node_modules/socket.io-client/dist/socket.io.msgpack.min.js.map +1 -1
  59. package/node_modules/socket.io-client/package.json +2 -2
  60. package/node_modules/undici/README.md +10 -1
  61. package/node_modules/undici/docs/docs/api/Agent.md +8 -8
  62. package/node_modules/undici/docs/docs/api/DiagnosticsChannel.md +9 -6
  63. package/node_modules/undici/docs/docs/api/Dispatcher.md +9 -0
  64. package/node_modules/undici/docs/docs/api/Errors.md +25 -0
  65. package/node_modules/undici/docs/docs/api/Fetch.md +10 -0
  66. package/node_modules/undici/docs/docs/api/Interceptors.md +4 -3
  67. package/node_modules/undici/docs/docs/api/Pool.md +5 -6
  68. package/node_modules/undici/docs/docs/index.md +10 -1
  69. package/node_modules/undici/lib/api/readable.js +1 -2
  70. package/node_modules/undici/lib/core/request.js +23 -2
  71. package/node_modules/undici/lib/core/util.js +0 -39
  72. package/node_modules/undici/lib/dispatcher/client-h1.js +11 -4
  73. package/node_modules/undici/lib/dispatcher/client-h2.js +52 -8
  74. package/node_modules/undici/lib/dispatcher/client.js +6 -1
  75. package/node_modules/undici/lib/dispatcher/env-http-proxy-agent.js +21 -6
  76. package/node_modules/undici/lib/dispatcher/pool-base.js +63 -2
  77. package/node_modules/undici/lib/dispatcher/pool.js +90 -7
  78. package/node_modules/undici/lib/dispatcher/proxy-agent.js +16 -3
  79. package/node_modules/undici/lib/handler/deduplication-handler.js +24 -11
  80. package/node_modules/undici/lib/handler/redirect-handler.js +5 -0
  81. package/node_modules/undici/lib/handler/retry-handler.js +72 -13
  82. package/node_modules/undici/lib/interceptor/cache.js +4 -1
  83. package/node_modules/undici/lib/interceptor/decompress.js +236 -69
  84. package/node_modules/undici/lib/web/cache/cache.js +17 -8
  85. package/node_modules/undici/lib/web/cache/cachestorage.js +11 -5
  86. package/node_modules/undici/lib/web/cookies/index.js +19 -7
  87. package/node_modules/undici/lib/web/cookies/util.js +3 -1
  88. package/node_modules/undici/lib/web/eventsource/eventsource.js +25 -1
  89. package/node_modules/undici/lib/web/fetch/body.js +22 -15
  90. package/node_modules/undici/lib/web/fetch/data-url.js +6 -6
  91. package/node_modules/undici/lib/web/fetch/formdata.js +30 -36
  92. package/node_modules/undici/lib/web/fetch/headers.js +34 -31
  93. package/node_modules/undici/lib/web/fetch/index.js +46 -25
  94. package/node_modules/undici/lib/web/fetch/request.js +74 -79
  95. package/node_modules/undici/lib/web/fetch/response.js +43 -44
  96. package/node_modules/undici/lib/web/fetch/util.js +7 -7
  97. package/node_modules/undici/lib/web/infra/index.js +16 -1
  98. package/node_modules/undici/lib/web/webidl/index.js +2 -14
  99. package/node_modules/undici/lib/web/websocket/connection.js +33 -12
  100. package/node_modules/undici/lib/web/websocket/events.js +43 -26
  101. package/node_modules/undici/lib/web/websocket/receiver.js +2 -0
  102. package/node_modules/undici/lib/web/websocket/stream/websocketerror.js +13 -10
  103. package/node_modules/undici/lib/web/websocket/stream/websocketstream.js +19 -4
  104. package/node_modules/undici/lib/web/websocket/websocket.js +44 -41
  105. package/node_modules/undici/package.json +1 -1
  106. package/node_modules/undici/types/diagnostics-channel.d.ts +2 -1
  107. package/node_modules/undici/types/interceptors.d.ts +1 -1
  108. package/node_modules/undici/types/webidl.d.ts +13 -7
  109. package/package.json +2 -2
@@ -31,6 +31,7 @@ func DevEnvCmd() *cobra.Command {
31
31
  devEnvEnvvarCmd(),
32
32
  devEnvImportCmd(),
33
33
  )
34
+ installDevEnvTelemetry(root)
34
35
  return root
35
36
  }
36
37
 
@@ -116,9 +116,13 @@ func envvarSetCmd() *cobra.Command {
116
116
  // a valid shell identifier and the container silently ignores it.
117
117
  name := strings.TrimSpace(args[0])
118
118
  if name == "" {
119
+ devEnvTrackingSpecial(cmd, "dev_env_envvar_set_invalid_name")
120
+ devEnvTrackingCancelled(cmd)
119
121
  return errors.New("variable name is required")
120
122
  }
121
123
  if err := envvar.ValidateName(name); err != nil {
124
+ devEnvTrackingSpecial(cmd, "dev_env_envvar_set_invalid_name")
125
+ devEnvTrackingCancelled(cmd)
122
126
  return err
123
127
  }
124
128
  debuglog.Printf(cmd.Context(), "@automattic/vip:bin:config:envvar", "Will set variable %q for environment %q: fromFile=%t", name, slug, fromFile != "")
@@ -138,6 +142,8 @@ func envvarSetCmd() *cobra.Command {
138
142
  default:
139
143
  value, err = appctx.Input(cmd, fmt.Sprintf("Value for %s", name), "")
140
144
  if err != nil {
145
+ devEnvTrackingSpecial(cmd, "dev_env_envvar_set_user_cancelled_input")
146
+ devEnvTrackingCancelled(cmd)
141
147
  return err
142
148
  }
143
149
  }
@@ -324,6 +324,7 @@ func runDevEnvCreate(cmd *cobra.Command, _ []string) error {
324
324
  if err != nil {
325
325
  return err
326
326
  }
327
+ devEnvTrackingCreate(cmd, cfg.Slug)
327
328
  ctx, finish := openDevEnvLog(cmd, cfg.Slug, true)
328
329
  defer finish()
329
330
  if err := devenv.Create(ctx, cfg); err != nil {
@@ -612,6 +613,7 @@ func devEnvStartCmd() *cobra.Command {
612
613
  msg := fmt.Sprintf("Found an existing Lando environment %q. vip-next will take it over — reusing its database and removing the old Lando containers (your data volume is kept). This process is irreversible. Continue?", slug)
613
614
  confirmed, cerr := appctx.Confirm(cmd, msg, false)
614
615
  if cerr == appctx.ErrNonInteractive || (!confirmed && cerr == nil) {
616
+ devEnvTrackingCancelled(cmd)
615
617
  fmt.Fprintln(cmd.OutOrStdout(), "Command cancelled")
616
618
  return nil
617
619
  }
@@ -690,6 +692,7 @@ func devEnvDestroyCmd() *cobra.Command {
690
692
  return err
691
693
  }
692
694
  if !ok {
695
+ devEnvTrackingCancelled(cmd)
693
696
  return nil
694
697
  }
695
698
  }
@@ -766,9 +769,13 @@ func devEnvPurgeCmd() *cobra.Command {
766
769
  return err
767
770
  }
768
771
  if !ok {
772
+ devEnvTrackingCancelled(cmd)
769
773
  return nil
770
774
  }
771
775
  }
776
+ if len(instancedata.AllNames()) > 0 {
777
+ devEnvTrackingBegin(cmd, map[string]any{"all": true})
778
+ }
772
779
  return devenv.Purge(cmd.Context(), soft)
773
780
  }}
774
781
  c.Flags().BoolVar(&yes, "yes", false, "Skip the confirmation prompt.")
@@ -31,7 +31,9 @@ import (
31
31
  // environment found" error / DEFAULT_SLUG fallback.
32
32
  func ResolveSlug(cmd *cobra.Command) (string, error) {
33
33
  if s, _ := cmd.Flags().GetString("slug"); s != "" {
34
- return nodeflags.ProcessSlug(s), nil
34
+ slug := nodeflags.ProcessSlug(s)
35
+ devEnvTrackingResolved(cmd, slug)
36
+ return slug, nil
35
37
  }
36
38
  if err := rejectAppEnvAlias(cmd); err != nil {
37
39
  return "", err
@@ -46,10 +48,15 @@ func ResolveSlug(cmd *cobra.Command) (string, error) {
46
48
  // options before calling getEnvironmentName (vip-dev-env-sync-sql.js:98).
47
49
  func ResolveLocalSlug(cmd *cobra.Command) (string, error) {
48
50
  if s, _ := cmd.Flags().GetString("slug"); s != "" {
49
- return nodeflags.ProcessSlug(s), nil
51
+ slug := nodeflags.ProcessSlug(s)
52
+ devEnvTrackingResolved(cmd, slug)
53
+ return slug, nil
50
54
  }
51
55
  slug, err := configFileSlug(cmd)
52
56
  if err != nil || slug != "" {
57
+ if err == nil {
58
+ devEnvTrackingResolved(cmd, slug)
59
+ }
53
60
  return slug, err
54
61
  }
55
62
  names := instancedata.AllNames()
@@ -57,10 +64,15 @@ func ResolveLocalSlug(cmd *cobra.Command) (string, error) {
57
64
  case 0:
58
65
  return "", errors.New("no dev environments found; create one with `vip dev-env create`")
59
66
  case 1:
67
+ devEnvTrackingResolved(cmd, names[0])
60
68
  return names[0], nil
61
69
  }
62
70
  if appctx.IsInteractive(cmd) {
63
- return appctx.Select(cmd, "Which environment?", names)
71
+ slug, err := appctx.Select(cmd, "Which environment?", names)
72
+ if err == nil {
73
+ devEnvTrackingResolved(cmd, slug)
74
+ }
75
+ return slug, err
64
76
  }
65
77
  return "", fmt.Errorf("multiple environments found; specify --slug: %w", appctx.ErrNonInteractive)
66
78
  }
@@ -0,0 +1,265 @@
1
+ package commands
2
+
3
+ import (
4
+ "bytes"
5
+ "context"
6
+ "encoding/json"
7
+ "errors"
8
+ "math"
9
+ "os"
10
+ "path/filepath"
11
+ "strings"
12
+ "time"
13
+
14
+ "github.com/Automattic/vip/internal/appctx"
15
+ "github.com/Automattic/vip/internal/devenv"
16
+ "github.com/Automattic/vip/internal/devenv/dockercli"
17
+ "github.com/Automattic/vip/internal/devenv/paths"
18
+ "github.com/Automattic/vip/internal/searchreplace"
19
+ "github.com/Automattic/vip/internal/telemetry"
20
+ "github.com/spf13/cobra"
21
+ )
22
+
23
+ type devEnvTelemetryKey struct{}
24
+
25
+ type devEnvTelemetryState struct {
26
+ name string
27
+ args []string
28
+ info map[string]any
29
+ started time.Time
30
+ canceled bool
31
+ }
32
+
33
+ var devEnvEventNames = map[string]string{
34
+ "create": "dev_env_create_command_", "start": "dev_env_start_command_",
35
+ "stop": "dev_env_stop_command_", "destroy": "dev_env_destroy_command_",
36
+ "info": "dev_env_info_command_", "list": "dev_env_list_command_",
37
+ "purge": "dev_env_purge_command_", "update": "dev_env_update_command_",
38
+ "exec": "dev_env_exec_command_", "shell": "dev_env_shell_command_",
39
+ "logs": "dev_env_logs_command_", "import sql": "dev_env_import_sql_command_",
40
+ "import media": "dev_env_import_media_command_", "sync sql": "dev_env_sync_sql_command_",
41
+ "envvar get": "dev_env_envvar_get_command_", "envvar get-all": "dev_env_envvar_get_all_command_",
42
+ "envvar list": "dev_env_envvar_list_command_", "envvar set": "dev_env_envvar_set_command_",
43
+ "envvar delete": "dev_env_envvar_delete_command_",
44
+ }
45
+
46
+ // installDevEnvTelemetry wraps leaves after their middleware is built. Slug
47
+ // commands emit execute from ResolveSlug, after resolution as Node does.
48
+ func installDevEnvTelemetry(root *cobra.Command) {
49
+ var visit func(*cobra.Command, string)
50
+ visit = func(parent *cobra.Command, prefix string) {
51
+ for _, child := range parent.Commands() {
52
+ path := strings.TrimSpace(prefix + " " + child.Name())
53
+ if child.RunE != nil {
54
+ if name, ok := devEnvEventNames[path]; ok {
55
+ original := child.RunE
56
+ child.RunE = func(cmd *cobra.Command, args []string) error {
57
+ state := &devEnvTelemetryState{name: name, args: args}
58
+ ctx := cmd.Context()
59
+ if ctx == nil {
60
+ ctx = context.Background()
61
+ }
62
+ cmd.SetContext(context.WithValue(ctx, devEnvTelemetryKey{}, state))
63
+ if path == "list" || ((path == "info" || path == "stop") && devEnvAllFlag(cmd)) {
64
+ devEnvTrackingBegin(cmd, map[string]any{"all": true})
65
+ }
66
+ err := original(cmd, args)
67
+ if state.started.IsZero() || state.canceled {
68
+ return err
69
+ }
70
+ if err != nil {
71
+ if path == "sync sql" && errors.Is(err, devenv.ErrEnvironmentNotStarted) {
72
+ props := devEnvTrackingCopy(state.info)
73
+ props["errorMessage"] = "Environment was not running"
74
+ trackEvent(name+"env_not_running_error", props)
75
+ return err
76
+ }
77
+ props := devEnvTrackingCopy(state.info)
78
+ props["failure"] = telemetry.ScrubErrorText(err.Error())
79
+ trackEvent(name+"error", props)
80
+ return err
81
+ }
82
+ props := devEnvTrackingCopy(state.info)
83
+ if path == "start" {
84
+ props["processing_time"] = int64(math.Ceil(time.Since(state.started).Seconds()))
85
+ }
86
+ trackEvent(name+"success", props)
87
+ return nil
88
+ }
89
+ }
90
+ }
91
+ visit(child, path)
92
+ }
93
+ }
94
+ visit(root, "")
95
+ }
96
+
97
+ func devEnvAllFlag(cmd *cobra.Command) bool {
98
+ if cmd.Flags().Lookup("all") == nil {
99
+ return false
100
+ }
101
+ all, _ := cmd.Flags().GetBool("all")
102
+ return all
103
+ }
104
+
105
+ func devEnvTrackingCopy(info map[string]any) map[string]any {
106
+ copyInfo := make(map[string]any, len(info))
107
+ for k, v := range info {
108
+ copyInfo[k] = v
109
+ }
110
+ return copyInfo
111
+ }
112
+
113
+ func devEnvTrackingState(cmd *cobra.Command) *devEnvTelemetryState {
114
+ ctx := cmd.Context()
115
+ if ctx == nil {
116
+ return nil
117
+ }
118
+ state, _ := ctx.Value(devEnvTelemetryKey{}).(*devEnvTelemetryState)
119
+ return state
120
+ }
121
+
122
+ func devEnvTrackingBegin(cmd *cobra.Command, info map[string]any) {
123
+ state := devEnvTrackingState(cmd)
124
+ if state == nil || !state.started.IsZero() {
125
+ return
126
+ }
127
+ state.info = devEnvTrackingCopy(info)
128
+ state.started = time.Now()
129
+ trackEvent(state.name+"execute", devEnvTrackingCopy(state.info))
130
+ }
131
+
132
+ func devEnvTrackingResolved(cmd *cobra.Command, slug string) {
133
+ state := devEnvTrackingState(cmd)
134
+ if state == nil {
135
+ return
136
+ }
137
+ info := devEnvTrackingInfo(slug)
138
+ if cmd.Name() == "sql" && cmd.Parent() != nil && cmd.Parent().Name() == "import" {
139
+ if len(state.args) > 0 {
140
+ details, err := searchreplace.GetSqlDumpDetails(state.args[0])
141
+ if err != nil {
142
+ return // Node determines dump type before its execute event.
143
+ }
144
+ info["sqldump_type"] = string(details.Type)
145
+ }
146
+ }
147
+ if cmd.Name() == "sql" && cmd.Parent() != nil && cmd.Parent().Name() == "sync" {
148
+ if ae := appctx.FromContext(cmd.Context()); ae != nil {
149
+ info = map[string]any{
150
+ "app": ae.App.ID, "env": ae.Env.UniqueLabel,
151
+ "slug": slug, "multisite": ae.Env.IsMultisite,
152
+ }
153
+ }
154
+ }
155
+ if cmd.Name() == "start" {
156
+ editor, _ := cmd.Flags().GetString("editor")
157
+ vscode, _ := cmd.Flags().GetBool("vscode")
158
+ if editor == "" && vscode {
159
+ editor = "vscode"
160
+ }
161
+ if editor != "" {
162
+ info["editor"] = editor
163
+ }
164
+ info["vscode"] = vscode
165
+ versions := (&dockercli.Runner{}).Versions(cmd.Context())
166
+ info["docker"] = versions.Engine
167
+ info["docker_compose"] = versions.Compose
168
+ info["compose_plugin"] = versions.ComposePlugin
169
+ }
170
+ devEnvTrackingBegin(cmd, info)
171
+ }
172
+
173
+ func devEnvTrackingCreate(cmd *cobra.Command, slug string) {
174
+ info := map[string]any{"slug": slug}
175
+ if app, _ := cmd.Flags().GetString("app"); app != "" {
176
+ info["app"] = app
177
+ }
178
+ if env, _ := cmd.Flags().GetString("env"); env != "" {
179
+ info["env"] = env
180
+ }
181
+ devEnvTrackingBegin(cmd, info)
182
+ }
183
+
184
+ func devEnvTrackingCancelled(cmd *cobra.Command) {
185
+ state := devEnvTrackingState(cmd)
186
+ if state != nil {
187
+ state.canceled = true
188
+ }
189
+ }
190
+
191
+ func devEnvTrackingSpecial(cmd *cobra.Command, name string) {
192
+ state := devEnvTrackingState(cmd)
193
+ if state != nil && !state.started.IsZero() {
194
+ trackEvent(name, devEnvTrackingCopy(state.info))
195
+ }
196
+ }
197
+
198
+ // devEnvTrackingInfo mirrors Node's getEnvTrackingInfo for the shared instance
199
+ // fields. Only known Node fields are eligible: Go also stores user environment
200
+ // variables and volume metadata in instance_data.json, neither of which belongs
201
+ // in telemetry. Local login credentials are excluded in both runtimes.
202
+ func devEnvTrackingInfo(slug string) map[string]any {
203
+ info := map[string]any{"slug": slug}
204
+ data, err := os.ReadFile(filepath.Join(paths.EnvironmentPath(slug), "instance_data.json"))
205
+ if err != nil {
206
+ return info
207
+ }
208
+ var fields map[string]json.RawMessage
209
+ if err := json.Unmarshal(data, &fields); err != nil {
210
+ return info
211
+ }
212
+ // Node's readEnvironmentData maps these legacy names before tracking.
213
+ if legacy, ok := fields["clientCode"]; ok && string(legacy) != "null" {
214
+ fields["appCode"] = legacy
215
+ }
216
+ for _, legacyKey := range []string{"enterpriseSearchEnabled", "elasticsearchEnabled"} {
217
+ if legacy, ok := fields[legacyKey]; ok && string(legacy) == "true" {
218
+ fields["elasticsearch"] = legacy
219
+ break
220
+ }
221
+ }
222
+ for _, key := range []string{
223
+ "siteSlug", "wpTitle", "multisite", "wordpress", "muPlugins", "appCode",
224
+ "mediaRedirectDomain", "phpmyadmin", "xdebug", "xdebugConfig", "mariadb",
225
+ "php", "elasticsearch", "mailpit", "photon", "cron", "pullAfter",
226
+ "version",
227
+ } {
228
+ raw, ok := fields[key]
229
+ if !ok {
230
+ continue
231
+ }
232
+ var value any
233
+ if key == "wordpress" || key == "muPlugins" || key == "appCode" {
234
+ var compact bytes.Buffer
235
+ if err := json.Compact(&compact, raw); err != nil {
236
+ continue
237
+ }
238
+ value = compact.String()
239
+ } else if err := json.Unmarshal(raw, &value); err != nil {
240
+ continue
241
+ }
242
+ if key == "php" {
243
+ if php, ok := value.(string); ok {
244
+ if colon := strings.IndexByte(php, ':'); colon >= 0 {
245
+ value = php[colon+1:]
246
+ }
247
+ }
248
+ }
249
+ info[devEnvSnakeCase(key)] = value
250
+ }
251
+ return info
252
+ }
253
+
254
+ func devEnvSnakeCase(key string) string {
255
+ var b strings.Builder
256
+ for _, r := range key {
257
+ if r >= 'A' && r <= 'Z' {
258
+ b.WriteByte('_')
259
+ b.WriteRune(r + ('a' - 'A'))
260
+ } else {
261
+ b.WriteRune(r)
262
+ }
263
+ }
264
+ return b.String()
265
+ }
@@ -0,0 +1,322 @@
1
+ package commands
2
+
3
+ import (
4
+ "errors"
5
+ "io"
6
+ "os"
7
+ "path/filepath"
8
+ "strings"
9
+ "testing"
10
+
11
+ "github.com/Automattic/vip/internal/devenv"
12
+ "github.com/Automattic/vip/internal/devenv/paths"
13
+ "github.com/Automattic/vip/internal/telemetry"
14
+ "github.com/spf13/cobra"
15
+ )
16
+
17
+ type devEnvCapturedEvent struct {
18
+ name string
19
+ props map[string]any
20
+ }
21
+
22
+ type devEnvEventClient struct{ events []devEnvCapturedEvent }
23
+
24
+ func (c *devEnvEventClient) TrackEvent(name string, props map[string]any) error {
25
+ copyProps := make(map[string]any, len(props))
26
+ for k, v := range props {
27
+ copyProps[k] = v
28
+ }
29
+ c.events = append(c.events, devEnvCapturedEvent{name: name, props: copyProps})
30
+ return nil
31
+ }
32
+
33
+ func TestDevEnvTrackingInfoOmitsCredentials(t *testing.T) {
34
+ t.Setenv("XDG_DATA_HOME", t.TempDir())
35
+ slug := "example-site"
36
+ if err := os.MkdirAll(paths.EnvironmentPath(slug), 0o755); err != nil {
37
+ t.Fatal(err)
38
+ }
39
+ data := []byte(`{"siteSlug":"example-site","wpTitle":"Example Site","php":"php-fpm:8.4","adminPassword":"local-password","autologinKey":"local-key","envVars":{"API_TOKEN":"private-value"},"overrides":"API_TOKEN: private-value"}`)
40
+ if err := os.WriteFile(filepath.Join(paths.EnvironmentPath(slug), "instance_data.json"), data, 0o600); err != nil {
41
+ t.Fatal(err)
42
+ }
43
+
44
+ got := devEnvTrackingInfo(slug)
45
+ if got["slug"] != slug || got["site_slug"] != slug || got["wp_title"] != "Example Site" || got["php"] != "8.4" {
46
+ t.Fatalf("safe tracking properties = %#v", got)
47
+ }
48
+ for _, key := range []string{"admin_password", "autologin_key", "env_vars", "overrides"} {
49
+ if _, ok := got[key]; ok {
50
+ t.Errorf("credential-bearing property %q was included", key)
51
+ }
52
+ }
53
+ }
54
+
55
+ func TestDevEnvTrackingInfoMissingRecord(t *testing.T) {
56
+ t.Setenv("XDG_DATA_HOME", t.TempDir())
57
+ got := devEnvTrackingInfo("missing")
58
+ if len(got) != 1 || got["slug"] != "missing" {
59
+ t.Fatalf("missing-record tracking properties = %#v, want slug only", got)
60
+ }
61
+ }
62
+
63
+ func TestDevEnvTrackingInfoMapsLegacyFields(t *testing.T) {
64
+ t.Setenv("XDG_DATA_HOME", t.TempDir())
65
+ slug := "legacy-site"
66
+ if err := os.MkdirAll(paths.EnvironmentPath(slug), 0o755); err != nil {
67
+ t.Fatal(err)
68
+ }
69
+ data := []byte(`{"siteSlug":"legacy-site","clientCode":{"repo":"example"},"elasticsearchEnabled":true}`)
70
+ if err := os.WriteFile(filepath.Join(paths.EnvironmentPath(slug), "instance_data.json"), data, 0o600); err != nil {
71
+ t.Fatal(err)
72
+ }
73
+ got := devEnvTrackingInfo(slug)
74
+ if got["app_code"] != `{"repo":"example"}` || got["elasticsearch"] != true {
75
+ t.Fatalf("legacy tracking properties = %#v", got)
76
+ }
77
+ if _, ok := got["client_code"]; ok {
78
+ t.Fatalf("legacy field was sent directly: %#v", got)
79
+ }
80
+ }
81
+
82
+ func TestDevEnvCommandTelemetryAfterSlugResolution(t *testing.T) {
83
+ t.Setenv("DO_NOT_TRACK", "")
84
+ t.Setenv("GO_ENV", "")
85
+ t.Setenv("NODE_ENV", "")
86
+ t.Setenv("XDG_DATA_HOME", t.TempDir())
87
+ client := &devEnvEventClient{}
88
+ old := GetConfig()
89
+ SetConfig(Config{Tracker: &telemetry.Tracker{Clients: []telemetry.Client{client}}})
90
+ t.Cleanup(func() { SetConfig(old) })
91
+
92
+ root := &cobra.Command{Use: "dev-env"}
93
+ info := &cobra.Command{Use: "info", RunE: func(cmd *cobra.Command, _ []string) error {
94
+ _, err := ResolveSlug(cmd)
95
+ return err
96
+ }}
97
+ addSlugFlag(info)
98
+ root.AddCommand(info)
99
+ installDevEnvTelemetry(root)
100
+ root.SetArgs([]string{"info", "--slug=example-site"})
101
+ if err := root.Execute(); err != nil {
102
+ t.Fatal(err)
103
+ }
104
+ if len(client.events) != 2 || client.events[0].name != "dev_env_info_command_execute" || client.events[1].name != "dev_env_info_command_success" {
105
+ t.Fatalf("events = %#v", client.events)
106
+ }
107
+ if client.events[0].props["slug"] != "example-site" {
108
+ t.Fatalf("execute properties = %#v", client.events[0].props)
109
+ }
110
+ }
111
+
112
+ func TestDevEnvTelemetryDoesNotStartBeforeSlugResolution(t *testing.T) {
113
+ t.Setenv("DO_NOT_TRACK", "")
114
+ t.Setenv("GO_ENV", "")
115
+ t.Setenv("NODE_ENV", "")
116
+ t.Setenv("XDG_DATA_HOME", t.TempDir())
117
+ t.Chdir(t.TempDir())
118
+ client := &devEnvEventClient{}
119
+ old := GetConfig()
120
+ SetConfig(Config{Tracker: &telemetry.Tracker{Clients: []telemetry.Client{client}}})
121
+ t.Cleanup(func() { SetConfig(old) })
122
+
123
+ root := &cobra.Command{Use: "dev-env"}
124
+ info := &cobra.Command{Use: "info", RunE: func(cmd *cobra.Command, _ []string) error {
125
+ _, err := ResolveSlug(cmd)
126
+ return err
127
+ }}
128
+ addSlugFlag(info)
129
+ root.AddCommand(info)
130
+ installDevEnvTelemetry(root)
131
+ root.SetArgs([]string{"info"})
132
+ if err := root.Execute(); err == nil {
133
+ t.Fatal("expected missing environment to fail")
134
+ }
135
+ if len(client.events) != 0 {
136
+ t.Fatalf("events before slug resolution = %#v", client.events)
137
+ }
138
+ }
139
+
140
+ func TestDevEnvTelemetryRecordsCommandError(t *testing.T) {
141
+ t.Setenv("DO_NOT_TRACK", "")
142
+ t.Setenv("GO_ENV", "")
143
+ t.Setenv("NODE_ENV", "")
144
+ t.Setenv("XDG_DATA_HOME", t.TempDir())
145
+ client := &devEnvEventClient{}
146
+ old := GetConfig()
147
+ SetConfig(Config{Tracker: &telemetry.Tracker{Clients: []telemetry.Client{client}}})
148
+ t.Cleanup(func() { SetConfig(old) })
149
+
150
+ root := &cobra.Command{Use: "dev-env"}
151
+ logs := &cobra.Command{Use: "logs", RunE: func(cmd *cobra.Command, _ []string) error {
152
+ if _, err := ResolveSlug(cmd); err != nil {
153
+ return err
154
+ }
155
+ return errors.New("log stream failed")
156
+ }}
157
+ addSlugFlag(logs)
158
+ root.AddCommand(logs)
159
+ installDevEnvTelemetry(root)
160
+ root.SetArgs([]string{"logs", "--slug=example-site"})
161
+ if err := root.Execute(); err == nil {
162
+ t.Fatal("expected logs to fail")
163
+ }
164
+ if len(client.events) != 2 || client.events[1].name != "dev_env_logs_command_error" || client.events[1].props["failure"] != "log stream failed" {
165
+ t.Fatalf("error events = %#v", client.events)
166
+ }
167
+ }
168
+
169
+ func TestDevEnvCreateEmitsCommandEvents(t *testing.T) {
170
+ t.Setenv("DO_NOT_TRACK", "")
171
+ t.Setenv("GO_ENV", "")
172
+ t.Setenv("NODE_ENV", "")
173
+ t.Setenv("XDG_DATA_HOME", t.TempDir())
174
+ t.Chdir(t.TempDir())
175
+ client := &devEnvEventClient{}
176
+ old := GetConfig()
177
+ SetConfig(Config{Tracker: &telemetry.Tracker{Clients: []telemetry.Client{client}}})
178
+ t.Cleanup(func() { SetConfig(old) })
179
+
180
+ root := DevEnvCmd()
181
+ root.SetOut(io.Discard)
182
+ root.SetErr(io.Discard)
183
+ root.SetArgs([]string{"create", "--slug=example-site", "--title=Example Site"})
184
+ if err := root.Execute(); err != nil {
185
+ t.Fatal(err)
186
+ }
187
+ if len(client.events) != 2 || client.events[0].name != "dev_env_create_command_execute" || client.events[1].name != "dev_env_create_command_success" {
188
+ t.Fatalf("create events = %#v", client.events)
189
+ }
190
+ if client.events[0].props["slug"] != "example-site" {
191
+ t.Fatalf("create properties = %#v", client.events[0].props)
192
+ }
193
+ }
194
+
195
+ func TestDevEnvImportSQLTracksDumpType(t *testing.T) {
196
+ t.Setenv("DO_NOT_TRACK", "")
197
+ t.Setenv("GO_ENV", "")
198
+ t.Setenv("NODE_ENV", "")
199
+ t.Setenv("XDG_DATA_HOME", t.TempDir())
200
+ file := filepath.Join(t.TempDir(), "export.sql")
201
+ if err := os.WriteFile(file, []byte("-- ordinary SQL export\nCREATE TABLE wp_posts (id int);\n"), 0o600); err != nil {
202
+ t.Fatal(err)
203
+ }
204
+ client := &devEnvEventClient{}
205
+ old := GetConfig()
206
+ SetConfig(Config{Tracker: &telemetry.Tracker{Clients: []telemetry.Client{client}}})
207
+ t.Cleanup(func() { SetConfig(old) })
208
+
209
+ root := &cobra.Command{Use: "dev-env"}
210
+ imp := &cobra.Command{Use: "import"}
211
+ sql := &cobra.Command{Use: "sql", RunE: func(cmd *cobra.Command, _ []string) error {
212
+ _, err := ResolveSlug(cmd)
213
+ return err
214
+ }}
215
+ addSlugFlag(sql)
216
+ imp.AddCommand(sql)
217
+ root.AddCommand(imp)
218
+ installDevEnvTelemetry(root)
219
+ root.SetArgs([]string{"import", "sql", "--slug=example-site", file})
220
+ if err := root.Execute(); err != nil {
221
+ t.Fatal(err)
222
+ }
223
+ if len(client.events) != 2 || client.events[0].props["sqldump_type"] != "MYSQLDUMP" {
224
+ t.Fatalf("import SQL events = %#v", client.events)
225
+ }
226
+ }
227
+
228
+ func TestDevEnvEnvvarSetInvalidNameEvent(t *testing.T) {
229
+ t.Setenv("DO_NOT_TRACK", "")
230
+ t.Setenv("GO_ENV", "")
231
+ t.Setenv("NODE_ENV", "")
232
+ t.Setenv("XDG_DATA_HOME", t.TempDir())
233
+ client := &devEnvEventClient{}
234
+ old := GetConfig()
235
+ SetConfig(Config{Tracker: &telemetry.Tracker{Clients: []telemetry.Client{client}}})
236
+ t.Cleanup(func() { SetConfig(old) })
237
+
238
+ root := DevEnvCmd()
239
+ root.SetOut(io.Discard)
240
+ root.SetErr(io.Discard)
241
+ root.SetArgs([]string{"envvar", "set", "--slug=example-site", "invalid-name", "value"})
242
+ if err := root.Execute(); err == nil {
243
+ t.Fatal("expected invalid variable name to fail")
244
+ }
245
+ if len(client.events) != 2 || client.events[0].name != "dev_env_envvar_set_command_execute" || client.events[1].name != "dev_env_envvar_set_invalid_name" {
246
+ t.Fatalf("invalid-name events = %#v", client.events)
247
+ }
248
+ }
249
+
250
+ func TestDevEnvEventNamesCoverEveryCommandLeaf(t *testing.T) {
251
+ root := DevEnvCmd()
252
+ seen := map[string]bool{}
253
+ var walk func(*cobra.Command, string)
254
+ walk = func(parent *cobra.Command, prefix string) {
255
+ for _, child := range parent.Commands() {
256
+ path := strings.TrimSpace(prefix + " " + child.Name())
257
+ if child.RunE != nil {
258
+ if _, ok := devEnvEventNames[path]; !ok {
259
+ t.Errorf("no event name for %q", path)
260
+ }
261
+ seen[path] = true
262
+ }
263
+ walk(child, path)
264
+ }
265
+ }
266
+ walk(root, "")
267
+ for path := range devEnvEventNames {
268
+ if !seen[path] {
269
+ t.Errorf("event name %q has no command leaf", path)
270
+ }
271
+ }
272
+ }
273
+
274
+ func TestDevEnvAllCommandUsesAllProperty(t *testing.T) {
275
+ t.Setenv("DO_NOT_TRACK", "")
276
+ t.Setenv("GO_ENV", "")
277
+ t.Setenv("NODE_ENV", "")
278
+ client := &devEnvEventClient{}
279
+ old := GetConfig()
280
+ SetConfig(Config{Tracker: &telemetry.Tracker{Clients: []telemetry.Client{client}}})
281
+ t.Cleanup(func() { SetConfig(old) })
282
+
283
+ root := &cobra.Command{Use: "dev-env"}
284
+ info := &cobra.Command{Use: "info", RunE: func(*cobra.Command, []string) error { return nil }}
285
+ info.Flags().Bool("all", false, "")
286
+ root.AddCommand(info)
287
+ installDevEnvTelemetry(root)
288
+ root.SetArgs([]string{"info", "--all"})
289
+ if err := root.Execute(); err != nil {
290
+ t.Fatal(err)
291
+ }
292
+ if len(client.events) != 2 || client.events[0].props["all"] != true || client.events[1].props["all"] != true {
293
+ t.Fatalf("all events = %#v", client.events)
294
+ }
295
+ }
296
+
297
+ func TestDevEnvSyncNotRunningUsesNodeEvent(t *testing.T) {
298
+ t.Setenv("DO_NOT_TRACK", "")
299
+ t.Setenv("GO_ENV", "")
300
+ t.Setenv("NODE_ENV", "")
301
+ client := &devEnvEventClient{}
302
+ old := GetConfig()
303
+ SetConfig(Config{Tracker: &telemetry.Tracker{Clients: []telemetry.Client{client}}})
304
+ t.Cleanup(func() { SetConfig(old) })
305
+
306
+ root := &cobra.Command{Use: "dev-env"}
307
+ sync := &cobra.Command{Use: "sync"}
308
+ sql := &cobra.Command{Use: "sql", RunE: func(cmd *cobra.Command, _ []string) error {
309
+ devEnvTrackingBegin(cmd, map[string]any{"slug": "example-site", "app": 123, "env": "develop", "multisite": false})
310
+ return devenv.ErrEnvironmentNotStarted
311
+ }}
312
+ sync.AddCommand(sql)
313
+ root.AddCommand(sync)
314
+ installDevEnvTelemetry(root)
315
+ root.SetArgs([]string{"sync", "sql"})
316
+ if err := root.Execute(); !errors.Is(err, devenv.ErrEnvironmentNotStarted) {
317
+ t.Fatalf("got error %v", err)
318
+ }
319
+ if len(client.events) != 2 || client.events[1].name != "dev_env_sync_sql_command_env_not_running_error" || client.events[1].props["errorMessage"] != "Environment was not running" {
320
+ t.Fatalf("sync events = %#v", client.events)
321
+ }
322
+ }