Skip to content

Commit d832707

Browse files
author
mike-delbalso_data
committed
Add authenticated AIR product feedback command
1 parent 814bdfe commit d832707

11 files changed

Lines changed: 433 additions & 1 deletion

File tree

‎.nextchanges/air/feedback.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* Add `databricks air feedback` to submit feedback with workspace authentication, agent or human attribution, and optional run context.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Cloud = false

‎acceptance/air/feedback/output.txt‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
2+
=== agent report with a receipt
3+
>>> [CLI] air feedback --message Logs omitted the failure reason --reporter agent --agent-name codex --run-id 123 -o json
4+
{
5+
"v": 1,
6+
"ts": "[TIMESTAMP]",
7+
"data": {
8+
"received": true,
9+
"feedback_id": "[UUID]"
10+
}
11+
}
12+
13+
=== human report
14+
>>> [CLI] air feedback --message The workaround succeeded
15+
Feedback received. Reference: [UUID]
16+
17+
=== validation errors
18+
>>> musterr [CLI] air feedback --message -o json
19+
true
20+
21+
>>> musterr [CLI] air feedback --message Logs omitted the failure reason --reporter robot -o json
22+
true
23+
24+
>>> musterr [CLI] air feedback --message Logs omitted the failure reason --agent-name codex -o json
25+
true
26+
27+
>>> musterr [CLI] air feedback unexpected --message Logs omitted the failure reason -o json
28+
true

‎acceptance/air/feedback/script‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
title "agent report with a receipt"
2+
trace $CLI air feedback --message "Logs omitted the failure reason" --reporter agent --agent-name codex --run-id 123 -o json
3+
4+
title "human report"
5+
trace $CLI air feedback --message "The workaround succeeded"
6+
7+
title "validation errors"
8+
trace musterr $CLI air feedback --message " " -o json | jq -e '.error.code == "INVALID_ARGUMENT"'
9+
trace musterr $CLI air feedback --message "Logs omitted the failure reason" --reporter robot -o json | jq -e '.error.code == "INVALID_ARGUMENT"'
10+
trace musterr $CLI air feedback --message "Logs omitted the failure reason" --agent-name codex -o json | jq -e '.error.code == "INVALID_ARGUMENT"'
11+
trace musterr $CLI air feedback unexpected --message "Logs omitted the failure reason" -o json | jq -e '.error.code == "INVALID_ARGUMENT"'

‎acceptance/air/feedback/test.toml‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Explicit feedback still submits when automatic usage telemetry is disabled.
2+
Env.DATABRICKS_CLI_DISABLE_TELEMETRY = "1"
3+
4+
[[Repls]]
5+
Old = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'
6+
New = '[FEEDBACK_ID]'

‎acceptance/air/help/output.txt‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Usage:
1111
Available Commands:
1212
cancel Cancel one or more runs
1313
convert-to-dabs Convert an AIR run YAML into a Databricks Asset Bundle
14+
feedback Send feedback about AI Runtime to Databricks
1415
get Show status, configuration, and timing details for a specified run
1516
images Manage container images for AI Runtime
1617
list List active runs for the current user
@@ -210,3 +211,39 @@ Global Flags:
210211
-o, --output type output type: text or json (default text)
211212
-p, --profile string ~/.databrickscfg profile
212213
-t, --target string bundle target to use (if applicable)
214+
215+
=== feedback help
216+
>>> [CLI] air feedback --help
217+
Send feedback about AI Runtime using your workspace authentication.
218+
219+
Include the command, observed problem, expected behavior, and any workaround
220+
in --message. Use --reporter agent for reports written by an agent.
221+
222+
Sends your message, CLI version, reporter type, and optional run ID and agent
223+
name to Fast Feedback. The service associates it with your workspace and
224+
authenticated principal. No transcripts, source files, or logs are collected.
225+
Do not include credentials or sensitive data. Follow-up contact is opt-in.
226+
227+
This is an explicit submission, independent of automatic usage telemetry.
228+
Success confirms receipt by the collector; processing into review tools is
229+
asynchronous.
230+
231+
Usage:
232+
databricks air feedback [flags]
233+
234+
Examples:
235+
databricks air feedback --message "air logs omitted the failure reason" --reporter agent --agent-name codex --run-id 123 -o json
236+
237+
Flags:
238+
--agent-name string Agent name; requires --reporter agent
239+
--can-be-contacted Allow follow-up about this feedback
240+
-h, --help help for feedback
241+
--message string Feedback message (1–10,000 characters)
242+
--reporter string Who wrote the feedback: human or agent (default "human")
243+
--run-id string Related job run ID
244+
245+
Global Flags:
246+
--debug enable debug logging
247+
-o, --output type output type: text or json (default text)
248+
-p, --profile string ~/.databrickscfg profile
249+
-t, --target string bundle target to use (if applicable)

‎acceptance/air/help/script‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,6 @@ trace $CLI air logs --help
2424

2525
title "cancel help"
2626
trace $CLI air cancel --help
27+
28+
title "feedback help"
29+
trace $CLI air feedback --help

‎cmd/air/air.go‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ These commands are under active development.`,
2828
cmd.AddCommand(newCancelCommand())
2929
cmd.AddCommand(newConvertToDabsCommand())
3030
cmd.AddCommand(newImagesCommand())
31+
cmd.AddCommand(newFeedbackCommand())
3132

3233
return cmd
3334
}

‎cmd/air/air_test.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ func TestNewRegistersAllSubcommands(t *testing.T) {
1616
registered[c.Name()] = true
1717
}
1818

19-
want := []string{"run", "get", "list", "pools", "logs", "cancel", "convert-to-dabs", "images"}
19+
want := []string{"run", "get", "list", "pools", "logs", "cancel", "convert-to-dabs", "images", "feedback"}
2020
for _, name := range want {
2121
assert.True(t, registered[name], "subcommand %q is not registered", name)
2222
}

‎cmd/air/feedback.go‎

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
package aircmd
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"errors"
7+
"fmt"
8+
"net/http"
9+
"strconv"
10+
"strings"
11+
"time"
12+
"unicode/utf8"
13+
"uuid"
14+
15+
"github.com/databricks/cli/cmd/root"
16+
"github.com/databricks/cli/internal/build"
17+
"github.com/databricks/cli/libs/auth"
18+
"github.com/databricks/cli/libs/cmdctx"
19+
"github.com/databricks/cli/libs/cmdio"
20+
"github.com/databricks/cli/libs/flags"
21+
"github.com/databricks/cli/libs/telemetry"
22+
"github.com/databricks/databricks-sdk-go/client"
23+
"github.com/spf13/cobra"
24+
)
25+
26+
const (
27+
feedbackMessageLimit = 10000
28+
feedbackAgentNameLimit = 100
29+
feedbackTimeout = 30 * time.Second
30+
)
31+
32+
type feedbackOptions struct {
33+
message string
34+
reporter string
35+
runID string
36+
agentName string
37+
canBeContacted bool
38+
}
39+
40+
func (o *feedbackOptions) validate() error {
41+
o.message = strings.TrimSpace(o.message)
42+
if o.message == "" || !utf8.ValidString(o.message) || utf8.RuneCountInString(o.message) > feedbackMessageLimit {
43+
return fmt.Errorf("--message must contain 1 to %d UTF-8 characters", feedbackMessageLimit)
44+
}
45+
if o.reporter != "human" && o.reporter != "agent" {
46+
return errors.New("--reporter must be human or agent")
47+
}
48+
if o.runID != "" {
49+
id, err := strconv.ParseInt(o.runID, 10, 64)
50+
if err != nil || id <= 0 {
51+
return errors.New("--run-id must be a positive job run ID")
52+
}
53+
}
54+
o.agentName = strings.TrimSpace(o.agentName)
55+
if o.agentName != "" && o.reporter != "agent" {
56+
return errors.New("--agent-name requires --reporter agent")
57+
}
58+
if !utf8.ValidString(o.agentName) || utf8.RuneCountInString(o.agentName) > feedbackAgentNameLimit {
59+
return fmt.Errorf("--agent-name must contain at most %d UTF-8 characters", feedbackAgentNameLimit)
60+
}
61+
return nil
62+
}
63+
64+
// These bindings mirror FastFeedbackLog in universe. Feedback text is sent only
65+
// through that explicitly submitted feedback schema, never a CLI usage event.
66+
type fastFeedbackLog struct {
67+
ReferenceEventID string `json:"reference_event_id"`
68+
ParentUI string `json:"parent_ui"`
69+
Origin string `json:"origin"`
70+
Text string `json:"text"`
71+
CanBeContacted bool `json:"can_be_contacted"`
72+
Product string `json:"product"`
73+
FeedbackSource string `json:"feedback_source"`
74+
ReporterType string `json:"reporter_type"`
75+
CLIVersion string `json:"cli_version"`
76+
RunID string `json:"run_id,omitempty"`
77+
AgentName string `json:"agent_name,omitempty"`
78+
}
79+
80+
type feedbackEvent struct {
81+
FrontendLogEventID string `json:"frontend_log_event_id"`
82+
Entry struct {
83+
FastFeedbackLog fastFeedbackLog `json:"fast_feedback_log"`
84+
} `json:"entry"`
85+
}
86+
87+
type feedbackReceipt struct {
88+
Received bool `json:"received"`
89+
FeedbackID string `json:"feedback_id"`
90+
}
91+
92+
func newFeedbackCommand() *cobra.Command {
93+
var o feedbackOptions
94+
cmd := &cobra.Command{
95+
Use: "feedback",
96+
Short: "Send feedback about AI Runtime to Databricks",
97+
Long: `Send feedback about AI Runtime using your workspace authentication.
98+
99+
Include the command, observed problem, expected behavior, and any workaround
100+
in --message. Use --reporter agent for reports written by an agent.
101+
102+
Sends your message, CLI version, reporter type, and optional run ID and agent
103+
name to Fast Feedback. The service associates it with your workspace and
104+
authenticated principal. No transcripts, source files, or logs are collected.
105+
Do not include credentials or sensitive data. Follow-up contact is opt-in.
106+
107+
This is an explicit submission, independent of automatic usage telemetry.
108+
Success confirms receipt by the collector; processing into review tools is
109+
asynchronous.`,
110+
Example: ` databricks air feedback --message "air logs omitted the failure reason" --reporter agent --agent-name codex --run-id 123 -o json`,
111+
}
112+
cmd.Flags().StringVar(&o.message, "message", "", "Feedback message (1–10,000 characters)")
113+
cmd.Flags().StringVar(&o.reporter, "reporter", "human", "Who wrote the feedback: human or agent")
114+
cmd.Flags().StringVar(&o.runID, "run-id", "", "Related job run ID")
115+
cmd.Flags().StringVar(&o.agentName, "agent-name", "", "Agent name; requires --reporter agent")
116+
cmd.Flags().BoolVar(&o.canBeContacted, "can-be-contacted", false, "Allow follow-up about this feedback")
117+
// Cobra runs Args before root initializes command IO. Validate here so JSON
118+
// errors can be rendered, while still rejecting input before authentication.
119+
cmd.PreRunE = func(cmd *cobra.Command, args []string) error {
120+
if err := cobra.NoArgs(cmd, args); err != nil {
121+
return renderError(cmd.Context(), cmd, "INVALID_ARGUMENT", "VALIDATION", false, err)
122+
}
123+
if err := o.validate(); err != nil {
124+
return renderError(cmd.Context(), cmd, "INVALID_ARGUMENT", "VALIDATION", false, err)
125+
}
126+
err := root.MustWorkspaceClient(cmd, args)
127+
if err == nil || errors.Is(err, root.ErrAlreadyPrinted) {
128+
return err
129+
}
130+
return renderError(cmd.Context(), cmd, "AUTHENTICATION_ERROR", "AUTHENTICATION", false, err)
131+
}
132+
cmd.RunE = func(cmd *cobra.Command, args []string) error {
133+
ctx := cmd.Context()
134+
apiClient, err := client.New(cmdctx.WorkspaceClient(ctx).Config)
135+
if err != nil {
136+
return renderError(ctx, cmd, "AUTHENTICATION_ERROR", "AUTHENTICATION", false, err)
137+
}
138+
id := uuid.New().String()
139+
if err := submitFeedback(ctx, apiClient, o, id); err != nil {
140+
// A timeout can occur after acceptance. Do not invite automated retries
141+
// that would create duplicate reports under a new submission ID.
142+
return renderError(ctx, cmd, "FEEDBACK_SUBMISSION_FAILED", "SUBMISSION", false,
143+
fmt.Errorf("feedback %s could not be confirmed: %w", id, err))
144+
}
145+
if root.OutputType(cmd) == flags.OutputJSON {
146+
return renderEnvelope(ctx, feedbackReceipt{Received: true, FeedbackID: id})
147+
}
148+
cmdio.LogString(ctx, "Feedback received. Reference: "+id)
149+
return nil
150+
}
151+
return cmd
152+
}
153+
154+
func submitFeedback(ctx context.Context, apiClient *client.DatabricksClient, o feedbackOptions, id string) error {
155+
ctx, cancel := context.WithTimeout(ctx, feedbackTimeout)
156+
defer cancel()
157+
event := feedbackEvent{FrontendLogEventID: id}
158+
event.Entry.FastFeedbackLog = fastFeedbackLog{
159+
ReferenceEventID: id,
160+
ParentUI: "ModelTraining.AIRuntime",
161+
Origin: "air-cli",
162+
Text: o.message,
163+
CanBeContacted: o.canBeContacted,
164+
Product: "PRODUCT_AIR",
165+
FeedbackSource: "SOURCE_AIR_CLI",
166+
ReporterType: "REPORTER_" + strings.ToUpper(o.reporter),
167+
CLIVersion: build.GetInfo().Version,
168+
RunID: o.runID,
169+
AgentName: o.agentName,
170+
}
171+
data, err := json.Marshal(event)
172+
if err != nil {
173+
return fmt.Errorf("encode feedback: %w", err)
174+
}
175+
var response telemetry.ResponseBody
176+
err = apiClient.Do(ctx, http.MethodPost, "/telemetry-ext", auth.WorkspaceIDHeaders(apiClient.Config), nil, telemetry.RequestBody{
177+
UploadTime: time.Now().UnixMilli(),
178+
Items: []string{},
179+
ProtoLogs: []string{string(data)},
180+
}, &response)
181+
if err != nil {
182+
return fmt.Errorf("submit feedback: %w", err)
183+
}
184+
if len(response.Errors) != 0 || response.NumProtoSuccess != 1 {
185+
return errors.New("collector did not acknowledge the feedback")
186+
}
187+
return nil
188+
}

0 commit comments

Comments
 (0)