Skip to content

aitools: log install-hint telemetry and bound telemetry uploads - #7467

Open
anton-107 wants to merge 1 commit into
mainfrom
aitools/install-hint-telemetry
Open

anton-107 wants to merge 1 commit into
mainfrom
aitools/install-hint-telemetry

Conversation

@anton-107

@anton-107 anton-107 commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

Summary

The CLI logs a new aitools_hint_event each time it shows a recommendation to install Databricks AI tools, so hints shown can be compared with installs that follow. It also bounds the whole telemetry upload, including client construction and authentication, by the upload timeout.

 MaybeHint(ctx, cmd)              # non-aitools command run by Claude Code without the tools
   cmdio.LogString(hintText)
+  logHintShown(INSTALL_TEXT)
 runCreate (apps init)
   flags mode, skills missing
-    cmdio.LogString("Tip: ...")
+    PrintSkillsTip                # prints the same tip
+      logHintShown(APPS_INIT_TIP)
   otherwise: RecommendSkillsInstall, skills missing
     no prompt possible
-      cmdio.LogString("Tip: ...")
+      PrintSkillsTip              # APPS_INIT_TIP
+    logHintShown(APPS_INIT_PROMPT)  # before asking, so a declined prompt counts
     AskYesOrNo(...)

Wire shape (one entry per hint shown):

{"aitools_hint_event": {"hint_kind": "INSTALL_TEXT"}}
  • The event carries only the hint kind. The command it was shown on is the entry's execution_context.command. The calling agent is not a field: the SDK already sends agent/<name> in the User-Agent of the telemetry upload.
  • AitoolsHintKind also declares CLAUDE_CODE_NATIVE and AGENT_INSTALL_TEXT, which nothing emits yet.
  • One apps init run can log two events (INSTALL_TEXT from the root hint and APPS_INIT_TIP), so count per hint_kind.
  • The apps init tip and prompt now also stay silent when every detected agent has the databricks plugin in its own plugin manifest, for every project or for the working directory, not only when the shared skills are installed (HasDatabricksToolingInstalled, reusing databricksPluginActiveIn). A detected agent without either still gets the recommendation.

Upload timeout (libs/telemetry/logger.go). The SDK resolves the config (including host metadata discovery) and fetches tokens with context.Background(), so before this change the upload timeout bounded only the HTTP requests. A slow token endpoint or host could hold the command's exit far past the timeout.

 Upload(ctx)
-  client.New(cfg)                       # unbounded: config resolution, host metadata
   ctx = WithTimeout(uploadTimeout)      # 3s, DATABRICKS_CLI_TELEMETRY_TIMEOUT
+  within(ctx, newAPIClient(cfg))        # client construction gives up at the deadline
+    AuthVisitor = authenticate          # every request, SDK retries included:
+      within(ctx, cfg.Authenticate)     # gives up at the deadline
   for 3 attempts: attempt(ctx, ...)     # requests stay on the caller's goroutine

An upload whose events are all hint events uses a 1s timeout instead of the 3s default (uploadTimeout), since most commands that show a hint would otherwise not upload at all. DATABRICKS_CLI_TELEMETRY_TIMEOUT, when set, applies to every upload. A hinted command whose own auth failed still tries the upload, so it runs the credential chain again at exit, bounded by the same 1s.

On timeout the goroutine started by within is abandoned; a panic in it is returned as an error and logged like any upload failure. cmd/root runs Upload as the last step before the process exits; the ssh agent shim runs it just before exec. If that exec fails, cmd/root uploads again on the same logger; while an abandoned goroutine of the first upload still runs, that second Upload is skipped, so it does not use the config concurrently.

Supporting changes:

libs/telemetry/logger.go           BufferedEvents(ctx): read buffered events from tests in other packages
libs/testserver/handlers.go        POST /telemetry-ext accepts every log in the request
acceptance/cmd/aitools/telemetry/  print_telemetry helper + `hint` and `apps-init-tip` cases
libs/aitools/agents/hint_test.go   uses testutil.SetAgentEnv instead of its own agent env list

Evidence

Upload timeout, with Upload temporarily switched back to client.New(cfg) (the token source, host metadata resolver and token refresh block until the test ends), then at this PR's head:

  • Before:
    --- FAIL: TestTelemetryUploadTimeoutBoundsTokenAcquisition (10.01s)
        Upload did not return after its timeout
    --- FAIL: TestTelemetryUploadTimeoutBoundsTokenRefreshOnRetry (10.01s)
        Upload did not return after its timeout
    --- FAIL: TestTelemetryUploadTimeoutBoundsConfigResolution (10.01s)
        Upload did not return after its timeout
    
    After:
    --- PASS: TestTelemetryUploadTimeoutBoundsTokenAcquisition (0.10s)    # timeout 0.1s
    --- PASS: TestTelemetryUploadTimeoutBoundsTokenRefreshOnRetry (2.00s)  # timeout 2s
    --- PASS: TestTelemetryUploadTimeoutBoundsConfigResolution (0.10s)    # timeout 0.1s
    

Hint-only timeout, at this PR's head:

--- PASS: TestUploadTimeout                               # hint-only 1s, mixed 3s, env overrides both
--- PASS: TestTelemetryUploadHintOnlyUsesShortTimeout (1.00s)  # stalled server: gives up after 1s
--- PASS: TestTelemetryUploadHintOnlyWithUnresolvedConfig    # never-authenticated config (bundle dir): still uploads
--- PASS: TestTelemetryUploadHintOnlyWithFailingAuthIsBounded # preset auth type, failing credentials: gives up within 1s
--- PASS: TestTelemetryUploadSkipsWhileEarlierUploadRuns   # second Upload skipped while the first's goroutine runs
--- PASS: TestWithinReturnsPanicAsError

The last three fail with their checks removed.

Hint event, at this PR's head (these tests assert the buffered and uploaded events; they were not re-run here with the call sites reverted):

--- PASS: TestMaybeHint_ClaudeNotInstalledHints           # [INSTALL_TEXT]; the silent and throttled cases log nothing
--- PASS: TestRecommendSkillsInstallNonInteractive        # [APPS_INIT_TIP]
--- PASS: TestRecommendSkillsInstallInteractiveDecline    # [APPS_INIT_PROMPT], prompt declined
--- PASS: TestAitoolsEventsWireKeys                       # {"aitools_hint_event":{"hint_kind":"INSTALL_TEXT"}}
--- PASS: TestAccept/cmd/aitools/telemetry/hint           # jobs list uploads one event; version and an installed plugin upload none
--- PASS: TestAccept/cmd/aitools/telemetry/apps-init-tip  # INSTALL_TEXT + APPS_INIT_TIP; with the plugin installed, no hint, tip or event

apps init guard: TestRecommendSkillsInstallSkipsWhenPluginInstalled and the plugin case of apps-init-tip fail with the old skills-only guard and pass at this head. TestHasDatabricksToolingInstalled covers the plugin for every agent, for one of two agents, for another project only, shared skills, and neither; the one-of-two and other-project cases fail with an any-agent, any-project check.

Also passing at this head: go build ./..., ./task test-exp-aitools, go test ./libs/telemetry/... ./libs/aitools/... ./libs/testserver/ ./cmd/apps/... ./cmd/aitools/... ./cmd/air/... ./cmd/environments/... ./experimental/ssh/..., the full acceptance suite (go test ./acceptance), ./task lint-q, ./task fmt-q, ./task checks, ./task check-changelog, and go vet with GOOS=windows and GOOS=darwin. In ./cmd/root, TestWorkspaceClientOrPromptRejectsPATOnSPOGFromConfigFile fails locally both with and without this change.

Merge Danger

Door: two-way

The event is additive and reverting stops sending it. No upload to a real workspace was run here, only the acceptance test server and a marshal test that pins the wire keys.

Blast Radius: telemetry

  • Commands that show a hint now make one telemetry upload at exit. When the hint is the only event, the upload waits at most 1s if /telemetry-ext is slow or unreachable; commands that also log other events keep the 3s default. Before, most such commands (current-user me, jobs list, ...) logged nothing and skipped the upload. Hint and tip texts are unchanged.
  • Every command that uploads telemetry (any command that logged an event and resolved auth, and the ssh agent shim) now returns from the upload within the timeout even when token fetch or host metadata discovery hangs. It then goes on without uploading and logs the timeout (telemetry upload failed: ..., info level in cmd/root, debug in the shim); before, it waited for the SDK call to finish. Exit codes are unchanged: upload errors were and are only logged. The upload also abandons a U2M (databricks-cli) token refresh that is still running at the deadline: if the access token expired during a long command and the refresh outlasts the rest of the upload budget (3s, or 1s for a hint-only upload), the process exits before the refreshed token is stored. If the server already rotated the refresh token, the next command fails to refresh until databricks auth login. Before, Upload waited and the refreshed token was saved.
  • The install hint prints at most once per Claude Code session per hour (once per command without CLAUDE_CODE_SESSION_ID, or when the CLI cache is disabled or not writable). The apps init tip and prompt print on every run where a detected agent has neither the shared skills nor the plugin for the working directory.
  • Hint counts are a lower bound: events upload only when the command resolved auth (not for version, completion, configure or most auth subcommands), the upload succeeds and telemetry is enabled. Because the install hint prints once per session per hour, a session whose hinted command resolves no auth logs no install hint event for that hour (documented in libs/telemetry/protos/README.md). APPS_INIT_* events are also logged for humans who have an agent config dir; the event has no calling-agent field to tell them apart. An accepted APPS_INIT_PROMPT and a declined one look the same: the install the prompt starts logs no event.
  • Changelog fragments: .nextchanges/cli/telemetry-upload-timeout.md, .nextchanges/cli/apps-init-plugin-tip.md. The acceptance cases use bash and jq and were not run on Windows here; Windows and macOS were only cross-compiled and vetted.

This pull request and its description were written by Isaac.

@anton-107
anton-107 requested review from a team and lennartkats-db as code owners November 11, 2026 14:24
@anton-107 anton-107 added the ai-auto PRs made by AI loops & factories, label Nov 11, 2026
@anton-107
anton-107 requested review from a team as code owners November 11, 2026 14:24
@github-actions

github-actions Bot commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

Commit: 2c02b391e | Workflow run

Regression test report

Selected 2 changed tests (limit=20, 0 not selected)

2 tests: regression=2 unreleased=0 coverage=0 failing=0
test branch main (634e6bf) latest (v1.26.0)
TestAccept/cmd/aitools/telemetry/apps-init-tip ✅ ❌ ❌
TestAccept/cmd/aitools/telemetry/hint ✅ ❌ ❌
TestAccept/cmd/aitools/telemetry/apps-init-tip: branch ✅ | main (634e6bf) ❌ | latest (v1.26.0) ❌

main (634e6bf):

acceptance_test.go:1373: 
        	Error Trace:	/home/runner/work/cli/cli/libs/testdiff/testdiff.go:22
        	            				/home/runner/work/cli/cli/acceptance/acceptance_test.go:1373
        	            				/home/runner/work/cli/cli/acceptance/acceptance_test.go:1114
        	            				/home/runner/work/cli/cli/acceptance/acceptance_test.go:660
        	Error:      	Not equal: 
        	            	expected: "\n=== apps init with flags prints the tip: one hint-shown event per hint printed\n\n>>> [CLI] apps init --template ./template --skip-install --name app-tip\nDatabricks skills are not installed. To work with Databricks reliably, first run: databricks aitools install\n✔ Creating project\n\n✔ Project created successfully!\n\n  Location: [TEST_TMP_DIR]/app-tip\n  Files: 3\n\nTip: coding agents detected without Databricks skills. Run 'databricks aitools install' to install them.\n\n>>> print_telemetry\n{\"command\":\"apps_init\",\"exit_code\":0,\"aitools_hint_event\":{\"hint_kind\":\"INSTALL_TEXT\"}}\n{\"command\":\"apps_init\",\"exit_code\":0,\"aitools_hint_event\":{\"hint_kind\":\"APPS_INIT_TIP\"}}\n\n=== plugin installed: no hint, no tip, no event\n\n>>> [CLI] apps init --template ./template --skip-install --name app-plugin\n✔ Creating project\n\n✔ Project created successfully!\n\n  Location: [TEST_TMP_DIR]/app-plugin\n  Files: 3\n\n\n>>> print_telemetry\n"
        	            	actual  : "\n=== apps init with flags prints the tip: one hint-shown event per hint printed\n\n>>> [CLI] apps init --template ./template --skip-install --name app-tip\nDatabricks skills are not installed. To work with Databricks reliably, first run: databricks aitools install\n✔ Creating project\n\n✔ Project created successfully!\n\n  Location: [TEST_TMP_DIR]/app-tip\n  Files: 3\n\nTip: coding agents detected without Databricks skills. Run 'databricks aitools install' to install them.\n\n>>> print_telemetry\n\n=== plugin installed: no hint, no tip, no event\n\n>>> [CLI] apps init --template ./template --skip-install --name app-plugin\n✔ Creating project\n\n✔ Project created successfully!\n\n  Location: [TEST_TMP_DIR]/app-plugin\n  Files: 3\n\nTip: coding agents detected without Databricks skills. Run 'databricks aitools install' to install them.\n\n>>> print_telemetry\n"
        	            	
        	            	Diff:
        	            	--- Expected
        	            	+++ Actual
        	            	@@ -15,4 +15,2 @@
        	            	 >>> print_telemetry
        	            	-{"command":"apps_init","exit_code":0,"aitools_hint_event":{"hint_kind":"INSTALL_TEXT"}}
        	            	-{"command":"apps_init","exit_code":0,"aitools_hint_event":{"hint_kind":"APPS_INIT_TIP"}}
        	            	 
        	            	@@ -28,2 +26,3 @@
        	            	 
        	            	+Tip: coding agents detected without Databricks skills. Run 'databricks aitools install' to install them.
        	            	 
        	Test:       	TestAccept/cmd/aitools/telemetry/apps-init-tip
        	Messages:   	cmd/aitools/telemetry/apps-init-tip/output.txt vs /tmp/TestAcceptcmdaitoolstelemetryapps-init-tip1049527418/002/output.txt
    acceptance_test.go:1151: LOG.apps-init: 
    acceptance_test.go:1151: LOG.apps-init-plugin:

latest (v1.26.0):

acceptance_test.go:1373: 
        	Error Trace:	/home/runner/work/cli/cli/libs/testdiff/testdiff.go:22
        	            				/home/runner/work/cli/cli/acceptance/acceptance_test.go:1373
        	            				/home/runner/work/cli/cli/acceptance/acceptance_test.go:1114
        	            				/home/runner/work/cli/cli/acceptance/acceptance_test.go:660
        	Error:      	Not equal: 
        	            	expected: "\n=== apps init with flags prints the tip: one hint-shown event per hint printed\n\n>>> [CLI] apps init --template ./template --skip-install --name app-tip\nDatabricks skills are not installed. To work with Databricks reliably, first run: databricks aitools install\n✔ Creating project\n\n✔ Project created successfully!\n\n  Location: [TEST_TMP_DIR]/app-tip\n  Files: 3\n\nTip: coding agents detected without Databricks skills. Run 'databricks aitools install' to install them.\n\n>>> print_telemetry\n{\"command\":\"apps_init\",\"exit_code\":0,\"aitools_hint_event\":{\"hint_kind\":\"INSTALL_TEXT\"}}\n{\"command\":\"apps_init\",\"exit_code\":0,\"aitools_hint_event\":{\"hint_kind\":\"APPS_INIT_TIP\"}}\n\n=== plugin installed: no hint, no tip, no event\n\n>>> [CLI] apps init --template ./template --skip-install --name app-plugin\n✔ Creating project\n\n✔ Project created successfully!\n\n  Location: [TEST_TMP_DIR]/app-plugin\n  Files: 3\n\n\n>>> print_telemetry\n"
        	            	actual  : "\n=== apps init with flags prints the tip: one hint-shown event per hint printed\n\n>>> [CLI] apps init --template ./template --skip-install --name app-tip\nDatabricks skills are not installed. To work with Databricks reliably, first run: databricks aitools install\n✔ Creating project\n\n✔ Project created successfully!\n\n  Location: [TEST_TMP_DIR]/app-tip\n  Files: 3\n\nTip: coding agents detected without Databricks skills. Run 'databricks aitools install' to install them.\n\n>>> print_telemetry\n\n=== plugin installed: no hint, no tip, no event\n\n>>> [CLI] apps init --template ./template --skip-install --name app-plugin\n✔ Creating project\n\n✔ Project created successfully!\n\n  Location: [TEST_TMP_DIR]/app-plugin\n  Files: 3\n\nTip: coding agents detected without Databricks skills. Run 'databricks aitools install' to install them.\n\n>>> print_telemetry\n"
        	            	
        	            	Diff:
        	            	--- Expected
        	            	+++ Actual
        	            	@@ -15,4 +15,2 @@
        	            	 >>> print_telemetry
        	            	-{"command":"apps_init","exit_code":0,"aitools_hint_event":{"hint_kind":"INSTALL_TEXT"}}
        	            	-{"command":"apps_init","exit_code":0,"aitools_hint_event":{"hint_kind":"APPS_INIT_TIP"}}
        	            	 
        	            	@@ -28,2 +26,3 @@
        	            	 
        	            	+Tip: coding agents detected without Databricks skills. Run 'databricks aitools install' to install them.
        	            	 
        	Test:       	TestAccept/cmd/aitools/telemetry/apps-init-tip
        	Messages:   	cmd/aitools/telemetry/apps-init-tip/output.txt vs /tmp/TestAcceptcmdaitoolstelemetryapps-init-tip3374971903/002/output.txt
    acceptance_test.go:1151: LOG.apps-init: 
    acceptance_test.go:1151: LOG.apps-init-plugin: 
FAIL
TestAccept/cmd/aitools/telemetry/hint: branch ✅ | main (634e6bf) ❌ | latest (v1.26.0) ❌

main (634e6bf):

acceptance_test.go:1373: 
        	Error Trace:	/home/runner/work/cli/cli/libs/testdiff/testdiff.go:22
        	            				/home/runner/work/cli/cli/acceptance/acceptance_test.go:1373
        	            				/home/runner/work/cli/cli/acceptance/acceptance_test.go:1114
        	            				/home/runner/work/cli/cli/acceptance/acceptance_test.go:660
        	Error:      	Not equal: 
        	            	expected: "\n=== hint printed on an authenticated command: one hint-shown event is uploaded\n\n>>> [CLI] jobs list\nDatabricks skills are not installed. To work with Databricks reliably, first run: databricks aitools install\n\n>>> print_telemetry\n{\"command\":\"jobs_list\",\"exit_code\":0,\"aitools_hint_event\":{\"hint_kind\":\"INSTALL_TEXT\"}}\n\n=== hint printed on a command that resolves no auth: nothing is uploaded\n\n>>> [CLI] version\nDatabricks skills are not installed. To work with Databricks reliably, first run: databricks aitools install\nDatabricks CLI v[CLI_VERSION]\n\n>>> print_telemetry\n\n=== plugin installed: no hint, no event\n\n>>> [CLI] jobs list\n\n>>> print_telemetry\n"
        	            	actual  : "\n=== hint printed on an authenticated command: one hint-shown event is uploaded\n\n>>> [CLI] jobs list\nDatabricks skills are not installed. To work with Databricks reliably, first run: databricks aitools install\n\n>>> print_telemetry\n\n=== hint printed on a command that resolves no auth: nothing is uploaded\n\n>>> [CLI] version\nDatabricks skills are not installed. To work with Databricks reliably, first run: databricks aitools install\nDatabricks CLI v[CLI_VERSION]\n\n>>> print_telemetry\n\n=== plugin installed: no hint, no event\n\n>>> [CLI] jobs list\n\n>>> print_telemetry\n"
        	            	
        	            	Diff:
        	            	--- Expected
        	            	+++ Actual
        	            	@@ -7,3 +7,2 @@
        	            	 >>> print_telemetry
        	            	-{"command":"jobs_list","exit_code":0,"aitools_hint_event":{"hint_kind":"INSTALL_TEXT"}}
        	            	 
        	Test:       	TestAccept/cmd/aitools/telemetry/hint
        	Messages:   	cmd/aitools/telemetry/hint/output.txt vs /tmp/TestAcceptcmdaitoolstelemetryhint732595967/002/output.txt
FAIL

latest (v1.26.0):

acceptance_test.go:1373: 
        	Error Trace:	/home/runner/work/cli/cli/libs/testdiff/testdiff.go:22
        	            				/home/runner/work/cli/cli/acceptance/acceptance_test.go:1373
        	            				/home/runner/work/cli/cli/acceptance/acceptance_test.go:1114
        	            				/home/runner/work/cli/cli/acceptance/acceptance_test.go:660
        	Error:      	Not equal: 
        	            	expected: "\n=== hint printed on an authenticated command: one hint-shown event is uploaded\n\n>>> [CLI] jobs list\nDatabricks skills are not installed. To work with Databricks reliably, first run: databricks aitools install\n\n>>> print_telemetry\n{\"command\":\"jobs_list\",\"exit_code\":0,\"aitools_hint_event\":{\"hint_kind\":\"INSTALL_TEXT\"}}\n\n=== hint printed on a command that resolves no auth: nothing is uploaded\n\n>>> [CLI] version\nDatabricks skills are not installed. To work with Databricks reliably, first run: databricks aitools install\nDatabricks CLI v[CLI_VERSION]\n\n>>> print_telemetry\n\n=== plugin installed: no hint, no event\n\n>>> [CLI] jobs list\n\n>>> print_telemetry\n"
        	            	actual  : "\n=== hint printed on an authenticated command: one hint-shown event is uploaded\n\n>>> [CLI] jobs list\nDatabricks skills are not installed. To work with Databricks reliably, first run: databricks aitools install\n\n>>> print_telemetry\n\n=== hint printed on a command that resolves no auth: nothing is uploaded\n\n>>> [CLI] version\nDatabricks skills are not installed. To work with Databricks reliably, first run: databricks aitools install\nDatabricks CLI v[CLI_VERSION]\n\n>>> print_telemetry\n\n=== plugin installed: no hint, no event\n\n>>> [CLI] jobs list\n\n>>> print_telemetry\n"
        	            	
        	            	Diff:
        	            	--- Expected
        	            	+++ Actual
        	            	@@ -7,3 +7,2 @@
        	            	 >>> print_telemetry
        	            	-{"command":"jobs_list","exit_code":0,"aitools_hint_event":{"hint_kind":"INSTALL_TEXT"}}
        	            	 
        	Test:       	TestAccept/cmd/aitools/telemetry/hint
        	Messages:   	cmd/aitools/telemetry/hint/output.txt vs /tmp/TestAcceptcmdaitoolstelemetryhint2863804270/002/output.txt

@eng-dev-ecosystem-bot

eng-dev-ecosystem-bot commented Nov 11, 2026 •

Copy link
Copy Markdown
Collaborator

✅ Integration test report

Commit: 2c02b39

Run: 43200099415

Env 🔄​flaky 💚​RECOVERED ✅​pass 🙈​skip Time
💚​ aws linux-2core-8gb 1 270 17 10:49
💚​ aws-windows-latest-4core-16gb 1 272 15 9:30
💚​ azure linux-2core-8gb 1 269 17 8:43
💚​ azure-windows-latest-4core-16gb 1 271 15 8:16
💚​ gcp linux-2core-8gb 1 270 17 14:59
🔄​ gcp-windows-latest-4core-16gb 2 1 270 15 15:09
Test Name aws linux-2core-8gb aws-windows-latest-4core-16gb azure linux-2core-8gb azure-windows-latest-4core-16gb gcp linux-2core-8gb gcp-windows-latest-4core-16gb
💚​ TestAccept 💚​R 💚​R 💚​R 💚​R 💚​R 💚​R
🔄​ TestFilerRecursiveDelete ✅​p ✅​p ✅​p ✅​p ✅​p 🔄​f
🔄​ TestFilerRecursiveDelete/files ✅​p ✅​p ✅​p ✅​p ✅​p 🔄​f
Top 15 slowest tests (at least 2 minutes):
duration env testname
3:53 aws linux-2core-8gb TestAccept
3:46 aws-windows-latest-4core-16gb TestAccept
3:43 azure-windows-latest-4core-16gb TestAccept
3:41 gcp-windows-latest-4core-16gb TestAccept
3:40 azure linux-2core-8gb TestAccept
3:30 gcp linux-2core-8gb TestFsCpDirToDirWithOverwriteFlag/dbfs_to_dbfs
3:21 gcp linux-2core-8gb TestAccept
3:03 gcp-windows-latest-4core-16gb TestImportDirDoesNotOverwrite
2:47 gcp linux-2core-8gb TestFsCpDir/dbfs_to_uc-volumes
2:24 aws-windows-latest-4core-16gb TestFilerRecursiveDelete/dbfs
2:13 gcp linux-2core-8gb TestFsCpFileToFileWithOverwriteFlag/dbfs_to_dbfs
2:11 gcp-windows-latest-4core-16gb TestFilerReadWrite/dbfs
2:11 gcp linux-2core-8gb TestFsCpDir/dbfs_to_dbfs
2:08 gcp linux-2core-8gb TestFsCpSourceIsDirectoryButTargetIsFile/dbfs_to_dbfs
2:03 gcp-windows-latest-4core-16gb TestFsCpDirToDirWithOverwriteFlag/dbfs_to_dbfs

@simonfaltum simonfaltum left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Automated review from simonfaltum's review agent (pr-review skill) at 2ca5f55. Each finding was verified against the code; simonfaltum may follow up.

Not approving yet: 3 open design questions.

This PR logs an aitools_hint_event each time the CLI shows an install hint, the apps init tip or the apps init prompt. It also bounds the whole telemetry upload, including config resolution and token fetch, by the upload timeout. The event's wire shape matches the Universe proto, the timeout claim holds for SDK v0.197.0, and CI is green. Nothing is blocking. The main open points are: an in-flight U2M token refresh can be abandoned at exit; a few test and acceptance-script convention issues; and three questions on scope, exit latency and APPS_INIT_* data quality.

Questions

  1. The Upload timeout change in libs/telemetry/logger.go (newAPIClientWithin, newAPIClient, authenticate and the three timeout tests) changes exit behavior for every telemetry upload: bundle deploy, the ssh agent shim, air and environments. The hint event doesn't need it, and the hint_test.go agent-env cleanup is also separate. AGENTS.md asks for one change per PR. Would you split these into their own PRs? If they stay here, would you add a .nextchanges/cli fragment for the fix that keeps the CLI from hanging at exit when auth stalls?
  2. Commands that print the install hint (for example current-user me or jobs list) used to log nothing and skip the telemetry upload. They now wait at exit for POST /telemetry-ext, up to the 3s default. That happens once per Claude session per hour, or on every command when CLAUDE_CODE_SESSION_ID is unset. Is that cost acceptable for this metric? Or would a cheaper signal work, such as a shorter timeout for hint-only uploads or a User-Agent segment?
  3. The apps init guard (HasDatabricksSkillsInstalled) checks only skills dirs, so APPS_INIT_TIP and APPS_INIT_PROMPT are also logged for Claude Code users who already have the plugin. They are also logged for humans with an agent config dir, and calling_agent is not sent to tell them apart. Should the guard be fixed first, should APPS_INIT_* be held back for now, or should this be accepted and documented for whoever builds the dashboards?

Comments outside the changed lines

  • [nit] libs/aitools/agents/hint.go:53: "Best-effort: it writes only to stderr" is no longer true: MaybeHint now also buffers a telemetry event, which makes a command that resolved auth upload at exit. The same goes for "(best-effort, stderr only)" at cmd/root/root.go:93. Please mention the telemetry event in both.

Comment thread libs/telemetry/logger.go Outdated
// authenticate sets the auth headers on r, giving up when r's context, which
// carries the upload deadline, is done. It authenticates a clone so that an
// abandoned goroutine never writes to r.
func authenticate(r *http.Request, cfg *config.Config) error {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nice to have] authenticate abandons cfg.Authenticate at the upload deadline, including a databricks-cli (U2M) token refresh that is already in flight. Suppose the access token expired during a long command, and the refresh takes longer than what is left of the 3s budget. The process then exits before PersistentAuth stores the new token. If the server already rotated the refresh token, the next command fails to refresh until databricks auth login; #7468 documents the same hazard for its background auth. Before this PR, Upload waited and the refreshed token was saved.

Separately, the safety note at line 209 ("Callers run Upload just before the process exits or execs") does not hold when the agent shim's execProcess fails. cmd/root then reads the same cfg in EnrichAuthError and calls Upload again, while the abandoned goroutine may still be writing to it.

Please state the refresh risk in Merge Danger, or decide whether abandoning an in-flight refresh is acceptable here. Also narrow the line-209 comment to what callers actually guarantee.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stated the refresh risk in Merge Danger: an in-flight U2M (databricks-cli) refresh that outlasts the rest of the budget is abandoned, and if the server already rotated the refresh token, the next command needs databricks auth login. Abandoning it is in line with what the timeout is for (no hang at exit), so I kept the behavior.

The comment above within (it replaces the detached block) now says only what callers guarantee: the goroutine keeps running until the SDK call returns or the process ends, cmd/root uploads as the last step before exit, and the ssh agent shim uploads right before exec. If the exec fails, cmd/root reads the same config and uploads again while that goroutine may still be running.

In 0326a2e.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The exec-failure race is now fixed in 29d7bbc. The logger counts the goroutines of uploads it stopped waiting for. While one is still running, a later Upload on the same logger returns a previous telemetry upload is still running and doesn't touch the config. That second Upload is the one cmd/root runs when the shim's execProcess fails. TestTelemetryUploadSkipsWhileEarlierUploadRuns covers this.

The other read on that path doesn't race. EnrichAuthError returns before reading cfg unless the error is a 401/403 APIError, and an exec failure isn't one.

The refresh risk stays as documented. Merge Danger now names both budgets: 3s, or 1s for a hint-only upload.

}
cmdio.LogString(ctx, hintText)
LogHintShown(ctx, protos.AitoolsHintKindInstallText)
recordSessionHint(ctx)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nice to have] recordSessionHint marks the session as hinted even when this command's event can never upload. Take a Claude session whose first non-aitools command resolves no auth, e.g. databricks version or auth profiles:

  • the hint prints;
  • the event is dropped at exit, because there is no ConfigUsed;
  • for the next hour, later commands in that session print no hint and log nothing.

So the session contributes zero hint events even though the agent saw the hint. That skews "hints shown vs installs" low. Please document this session-level loss next to the "uploads only when the command resolved auth" note in protos/README.md, or decide whether the metric can accept it before dashboards depend on it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Documented it in libs/telemetry/protos/README.md, right after the "uploads only when the command resolved auth" sentence: because the hint prints once per Claude Code session per hour, a session whose hinted command resolves no auth (e.g. databricks version) logs no install hint event for that hour. Merge Danger says the same. In 0326a2e.

unset CLAUDE_CONFIG_DIR

title "hint printed on an authenticated command: one hint-shown event is uploaded\n"
trace $CLI current-user me > /dev/null

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nice to have] This case relies on current-user me printing the hint. Open #7452 adds current-user me, along with auth, configure, help and completion, to the hint-exempt commands. Whichever PR lands second breaks this golden: no hint is printed and no event is uploaded (lines 9 and 18). A command that #7452 keeps hinting and the testserver serves, such as jobs list, avoids the conflict.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Switched both cases to jobs list. #7452 keeps hinting on it and the testserver serves it, so the golden holds in either merge order. In 0326a2e.

Comment thread libs/telemetry/logger.go Outdated
// before the process exits or execs, so it does not outlive them.

// newAPIClientWithin runs newAPIClient, giving up when ctx is done.
func newAPIClientWithin(ctx context.Context, cfg *config.Config) (*client.DatabricksClient, error) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nice to have] newAPIClientWithin and authenticate repeat the same goroutine/select/abandon logic. newAPIClient re-implements client.New by hand only to swap the AuthVisitor, so later SDK changes to client.New won't reach this path. Instead, run the client construction and the attempt loop in one goroutine, and select on its result against ctx.Done(). That bounds the same steps with one mechanism: config resolution, the token fetch on every attempt, and the requests. It also keeps client.New, and drops the request clone and the detached 10-line comment block above these functions.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried the single goroutine (client.New plus the attempt loop, selected against ctx.Done()), and it broke acceptance/telemetry/timeout. At the deadline, Upload returns on ctx.Done() before the request goroutine reports its own context deadline exceeded. So the --debug lines for the request, non-retriable error and Attempt 1 failed due to a timeout are lost or printed after telemetry upload failed, depending on scheduling, and the logged error loses the Post "<url>/telemetry-ext" detail. Requests honor ctx, so the patch keeps them on the caller's goroutine and moves only the calls that ignore ctx off it.

What I changed instead:

  • The duplicated goroutine/select/abandon code is now one helper, within(ctx, f). Both client construction and authenticate use it.
  • The detached comment block became that helper's doc comment.

I kept newAPIClient, because the per-request token fetch (including the one on a retry) can only be bounded through the AuthVisitor. client.New has no option to set it. I kept the request clone too: it stops an abandoned cfg.Authenticate from writing to the headers of a request that is already being sent. In 0326a2e.

# recorded requests so the next call sees only the next command's upload.
print_telemetry() {
if [ -f "$OUT_REQUESTS" ]; then
jq -c 'select(.path == "/telemetry-ext") | .body.protoLogs[] | fromjson

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nice to have] print_telemetry filters $OUT_REQUESTS with a hand-written jq 'select(.path == "/telemetry-ext") ...' and then rm. .agents/rules/testing.md says: "Filter recorded requests with print_requests.py, never with a hand-written jq 'select(...)' out.requests.txt pipeline — inline, or hidden inside a local print_requests() shell function." Wrapping the helper is allowed, so use jq only for decoding: print_requests.py --oneline //telemetry-ext | jq -c '.body.protoLogs[] | fromjson | ...'. Keep the [ -f "$OUT_REQUESTS" ] guard, because print_requests.py exits with an error when the file is missing (the version case).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

print_telemetry now wraps print_requests.py --oneline //telemetry-ext and uses jq only to decode protoLogs. The [ -f "$OUT_REQUESTS" ] guard stays for the version case, and print_requests.py deletes the file. In 0326a2e.

assert.Equal(t, []protos.AitoolsHintEvent{{HintKind: protos.AitoolsHintKindInstallText}}, hintEvents(t, ctx))
}

func TestMaybeHint_NotPrintedLogsNothing(t *testing.T) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] Several new tests repeat existing scenarios:

  • TestMaybeHint_NotPrintedLogsNothing repeats HumanCallerSilent, PluginInstalledSilent, AIToolsCommandSilent and NoIONoPanic.
  • TestMaybeHint_ThrottledSessionLogsOnce copies TestMaybeHint_ThrottledPerSession, including the inline newCtx := func() closure that .agents/rules/style-guide-go.md disallows.
  • The RecommendSkillsInstall* tests repeat recommend_test.go's NonInteractive, InteractiveDecline and SkipsWhenSkillsExist.

Wrapping the existing tests' ctx with telemetry.WithNewLogger and adding the event assertion gives the same coverage without a parallel suite. The RecommendSkillsInstall assertions would then live in recommend_test.go, next to recommend.go.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed hint_telemetry_test.go. The event assertions are now in the existing tests, each with its ctx wrapped in telemetry.WithNewLogger:

  • ClaudeNotInstalledHints: [INSTALL_TEXT].
  • HumanCallerSilent, PluginInstalledSilent, AIToolsCommandSilent and NoIONoPanic: no events.
  • ThrottledPerSession: one event, then none. It keeps its existing newCtx closure; I didn't add a new one.
  • recommend_test.go: NonInteractive gets [APPS_INIT_TIP], InteractiveDecline gets [APPS_INIT_PROMPT], SkipsWhenSkillsExist gets none.

In 0326a2e.

Comment thread libs/telemetry/logger.go
// BufferedEvents returns the events logged on ctx, or nil when ctx has no
// logger. Upload does not clear them. Tests in other packages use it to assert
// what a command logs without uploading.
func BufferedEvents(ctx context.Context) []protos.DatabricksCliLog {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] BufferedEvents is exported from libs/telemetry only for tests in other packages. The repo already checks events from other packages by handling POST /telemetry-ext on a testserver and calling telemetry.Upload (cmd/air/telemetry_test.go, cmd/environments/telemetry_test.go). Using that pattern keeps the buffer private. Likewise, LogHintShown is only called inside package agents and could be unexported.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LogHintShown is now unexported (logHintShown).

I kept BufferedEvents exported. The next PRs in this stack (#7469 and #7470) use it from cmd/aitools/funnel_test.go to check the install/update/uninstall funnel events command by command. Moving those to a testserver plus Upload would change those PRs. If you'd still rather keep it private, I'll make the change across the stack. In 0326a2e.

unset CLAUDE_CODE_SESSION_ID
unset CLAUDE_CONFIG_DIR
# A Claude Code config dir with no Databricks skills is what makes apps init print its tip.
mkdir -p "${USERPROFILE:-$HOME}/.claude"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] .agents/rules/testing.md says: "Commit static test inputs into the acceptance test directory; do not create them in script at test time." Commit home/.claude/ with a placeholder file, as hint/installed-home does, instead of mkdir -p "${USERPROFILE:-$HOME}/.claude". (sethome sets both variables, so the fallback is redundant.) The same file also sends traced output to > /dev/null, which the rules say should go to LOG.<name>.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The script now uses a committed apps-init-tip/home/.claude/.gitkeep instead of the mkdir. The apps init stdout goes to LOG.apps-init, not /dev/null. In 0326a2e.

"home",
]

[[Server]]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] This stub exists because libs/testserver/handlers.go:612 always returns numProtoSuccess: 1. With that default, the two-event apps init upload is a partial success and gets retried. .agents/rules/testing.md says to model API behavior in libs/testserver/ rather than in per-test [[Server]] stubs. Having the shared handler return len(protoLogs) would let this file, and the older stubs in acceptance/telemetry, drop the override.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The shared POST /telemetry-ext handler in libs/testserver/handlers.go now returns numProtoSuccess: len(protoLogs), and this file's [[Server]] stub is gone. The full acceptance suite passes with the new default. I left the older stubs in acceptance/telemetry alone: partial-success needs its stub, and dropping the others is a cleanup outside this PR. In 0326a2e.

Comment thread libs/telemetry/logger_test.go Outdated
server := testserver.New(t)
t.Cleanup(server.Close)

expiry := time.Now().Add(500 * time.Millisecond)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] expiry is fixed before Upload starts. If resolving the config and authenticating the first request take more than 500ms, the first attempt already sees an expired token, blocks in the second token fetch, and never reaches the server. count is then 0 and the test fails. That can happen on a loaded CI runner under -race. Setting expiry when the token source first returns a token, and sharing it with the handler via an atomic, removes the dependency on start-up time.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The token source now sets the expiry when it returns the first token, 500ms after that, and shares it with the handler through an atomic.Pointer[time.Time]. Start-up time no longer matters. In 0326a2e.

@anton-107
anton-107 force-pushed the aitools/install-hint-telemetry branch from 2ca5f55 to 0326a2e Compare November 11, 2026 16:15
@anton-107

Copy link
Copy Markdown
Contributor Author

Thanks for the review. All 11 inline comments are answered in their threads. Changes in 0326a2e:

  • Doc comments (outside the diff): MaybeHint in hint.go and the comment at cmd/root/root.go:93 now mention the telemetry event, which makes a command that resolved auth upload at exit.
  • logger.go: the duplicated goroutine/select logic is now one within(ctx, f) helper, and its doc comment says only what callers guarantee. I did not move the request loop onto the bounded goroutine: that made the --debug output of acceptance/telemetry/timeout racy (the thread has details). Merge Danger now states the risk of abandoning an in-flight U2M refresh.
  • logger_test.go: the token expiry is set when the first token is issued.
  • README: the session-level hint loss is documented in protos/README.md and in Merge Danger.
  • Unit tests: hint_telemetry_test.go is gone. The event assertions are now in the existing hint_test.go and recommend_test.go tests, and the no-op env workarounds in hint_test.go are removed.
  • Exports: LogHintShown is now unexported. BufferedEvents stays exported because aitools: add install funnel telemetry #7469 and aitools: add funnel telemetry to update, list, version and uninstall #7470 use it from cmd/aitools tests.
  • testserver: POST /telemetry-ext now returns numProtoSuccess: len(protoLogs), and the per-test stub is dropped.
  • Acceptance:

I haven't answered the three Questions in the review body yet; I'll follow up on them separately.

@anton-107
anton-107 force-pushed the aitools/install-hint-telemetry branch from 0326a2e to 673bfd5 Compare November 11, 2026 16:46
@anton-107

Copy link
Copy Markdown
Contributor Author

Rebased onto main as 673bfd5. #7003 gave title its own trailing newline, so the new hint and apps-init-tip scripts no longer pass \n; their golden files are unchanged. The thread replies that cite 0326a2e apply to this head too.

@anton-107
anton-107 force-pushed the aitools/install-hint-telemetry branch from 673bfd5 to e1c80b3 Compare November 11, 2026 16:56
@anton-107

anton-107 commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor Author

Answers to the three Questions, in e1c80b3:

  1. Scope. The logger.go timeout change stays in this PR. The hint event is what makes most of these commands upload at all, so bounding the upload belongs to the same change. Added .nextchanges/cli/telemetry-upload-timeout.md for the fix that keeps the CLI from hanging at exit when auth or the telemetry upload stalls. The hint_test.go env cleanup stays as well; after your nit it's down to the switch to testutil.SetAgentEnv.
  2. Exit latency. An upload whose events are all hint events now uses a 1s timeout. Any other upload keeps the existing timeout (DATABRICKS_CLI_TELEMETRY_TIMEOUT, default 3s). The env var, when set, still applies to every upload. The logic is in uploadTimeout in libs/telemetry/logger.go. TestUploadTimeout covers the choice, and TestTelemetryUploadHintOnlyUsesShortTimeout checks that a stalled server makes a hint-only upload give up after 1s. Merge Danger is updated.
  3. APPS_INIT_* guard. Fixed. apps init (flags-mode tip, non-interactive tip and prompt) now uses HasDatabricksToolingInstalled. It treats tooling as installed when a detected agent has the databricks plugin in its own manifest (the existing DatabricksPluginVersion) or the skills are installed. With the plugin installed, no tip or prompt is shown and nothing is logged. Tests: TestHasDatabricksToolingInstalled, TestRecommendSkillsInstallSkipsWhenPluginInstalled, and a plugin case in acceptance/cmd/aitools/telemetry/apps-init-tip; both of the latter fail with the old guard. Like the shared skills dir, the plugin of any one detected agent counts. The event still has no calling-agent field: adding one would change the event schema and is out of scope here, so APPS_INIT_* from humans with an agent config dir can't be told apart yet. Merge Danger says so.

@anton-107
anton-107 force-pushed the aitools/install-hint-telemetry branch from e1c80b3 to 71b4c1a Compare November 11, 2026 17:08

@simonfaltum simonfaltum left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Automated review from simonfaltum's review agent (pr-review skill) at 71b4c1a. Each finding was verified against the code; simonfaltum may follow up.

Not approving yet: 2 open design questions.

This PR logs an aitools_hint_event whenever the CLI shows the Claude Code install hint or the apps init tip or prompt. It also bounds the whole telemetry upload, including config resolution and token fetch, by its timeout, and keeps apps init quiet when an agent has the databricks plugin. The code does what the description says, CI is green, and the telemetry and agents tests pass under -race; nothing is blocking. Still open: accepted apps init prompts lead to installs that are never recorded, the abandoned token refresh from the earlier thread, a few nits, and two questions about scope and exit-time cost.

Questions

  1. This PR also changes how every telemetry upload is bounded (bundle deploy, the ssh agent shim, air and environments), including abandoning an in-flight U2M token refresh at the deadline. It also changes when apps init shows its tip. You explained that the hint event is what makes these commands upload, so the changes belong together. Would you be open to landing the upload-bounding change as its own PR, with this one on top, so it can be reviewed and reverted on its own? If you'd rather keep a single PR, could the title mention the upload change so it shows up in history?
  2. Hinted commands that used to skip telemetry now upload at exit and can wait up to 1s. cmdctx.SetConfigUsed runs before auth succeeds, so a hinted command whose own auth failed also reruns the default credential chain at exit (for example the Azure CLI strategy) until the 1s deadline. Is that cost acceptable for this metric? Or would you skip a hint-only upload when the command's credentials never resolved, for example when cfg.AuthType is empty?

Comments outside the changed lines

  • [nit] .github/PULL_REQUEST_TEMPLATE.md: .agents/skills/pr-checklist/SKILL.md asks PR bodies to use the template's ## Changes / ## Why / ## Tests headings in order, without new sections such as ## Summary. The Evidence block would fit under ## Tests. Also, Merge Danger says an in-flight U2M refresh is abandoned when it "outlasts the rest of the 3s budget". Hint-only uploads, which are the uploads this PR adds, get 1s (hintOnlyUploadTimeout), so please mention the 1s case too.

Agreeing with existing threads

  • [nice to have] Abandoned in-flight U2M refresh at the upload deadline; race on cfg when the shim's exec fails: #7467 (comment)

return nil
}

logHintShown(ctx, protos.AitoolsHintKindAppsInitPrompt)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nice to have] APPS_INIT_PROMPT is logged before the question, but nothing records the answer or the install it starts. When the user answers yes, installFn (installer.InstallAllSkills) installs the skills without logging an AitoolsInstallEvent; only cmd/aitools logs one. So in telemetry an accepted prompt and a declined one look the same, and this hint kind can't be compared with the installs that follow, which is what the event is for. Either log the outcome of the prompt path (an install event after installFn succeeds, or an accepted field once the proto has one), or note in libs/telemetry/protos/README.md that APPS_INIT_PROMPT conversions are not observable.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not in this PR. Recording the answer, or the install the prompt starts, needs a new field or event, and this stack doesn't change the telemetry schema. Recording it can follow separately. For now, Merge Danger says that an accepted APPS_INIT_PROMPT and a declined one look the same.

Comment thread cmd/apps/init.go
if flagsMode {
if !agents.HasDatabricksSkillsInstalled(ctx) {
cmdio.LogString(ctx, "Tip: coding agents detected without Databricks skills. Run 'databricks aitools install' to install them.")
if !agents.HasDatabricksToolingInstalled(ctx) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] apps init now skips its skills tip and its "Install skills now?" prompt when a detected agent has the databricks plugin, which users will notice. .agents/skills/pr-checklist/SKILL.md lists changed command behavior as needing a .nextchanges fragment, but the only fragment added covers the upload timeout. Please add one, e.g. .nextchanges/cli/apps-init-plugin-tip.md: "* apps init no longer suggests installing skills when a detected coding agent has the Databricks plugin. (#7467)"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added .nextchanges/cli/apps-init-plugin-tip.md with your wording and the PR link. In 29d7bbc.

Comment thread libs/telemetry/logger.go
err error
}
resc := make(chan result, 1)
go func() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] The goroutine in within has no recover. Config resolution and cfg.Authenticate used to run on the caller's goroutine, so a panic there (for example in a credentials strategy) was caught by the deferred recover in root.Execute, which prints the "unexpectedly had a fatal error" report. Now it is an unrecovered goroutine panic: a raw runtime crash at exit, after the command itself succeeded. Recovering inside the goroutine and sending the panic back as an error (resc <- result{err: fmt.Errorf("panic: %v", r)}) turns it into an ordinary "telemetry upload failed" log line.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The goroutine in within now recovers and returns the panic as an error (panic: <value>), so it shows up as an ordinary telemetry upload failed log line. TestWithinReturnsPanicAsError covers this. In 29d7bbc.

Comment thread libs/aitools/agents/skills.go Outdated
Comment on lines +45 to +51
func HasDatabricksToolingInstalled(ctx context.Context) bool {
for _, a := range DetectInstalled(ctx) {
if _, ok := a.DatabricksPluginVersion(ctx); ok {
return true
}
}
return HasDatabricksSkillsInstalled(ctx)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] The loop calls DetectInstalled, and then HasDatabricksSkillsInstalled calls it again, so each apps init probes every registry agent's config dir twice. .agents/rules/style-guide-go.md asks to avoid unnecessary loops. Call DetectInstalled once, return true when it is empty, check the plugins, then check the canonical and legacy skill dirs. A small hasSharedSkills(ctx) helper shared with HasDatabricksSkillsInstalled would do it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HasDatabricksToolingInstalled now calls DetectInstalled once: it returns true when no agent is detected, then checks the plugins, then the shared skills dirs. The canonical/legacy check moved into hasSharedSkills, which HasDatabricksSkillsInstalled also uses. In 29d7bbc.

Comment thread libs/aitools/agents/hint_telemetry.go Outdated
// The event is buffered and uploaded by cmd/root at exit, which needs resolved
// auth: a hint shown on a command that never resolves auth (e.g. `databricks
// version`) is not uploaded.
func logHintShown(ctx context.Context, kind protos.AitoolsHintKind) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] .agents/rules/testing.md says each source file should have a corresponding test file, and hint_telemetry.go has none. Its only function is already covered by the event assertions in hint_test.go and recommend_test.go. Moving logHintShown into hint.go keeps that coverage without adding an untested file or a duplicate test suite.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved logHintShown into hint.go and removed hint_telemetry.go. The event assertions in hint_test.go and recommend_test.go still cover it. In 29d7bbc.

@anton-107
anton-107 force-pushed the aitools/install-hint-telemetry branch from 71b4c1a to 29d7bbc Compare November 12, 2026 00:15
@anton-107 anton-107 changed the title aitools: log a telemetry event when an install hint is shown aitools: log install-hint telemetry and bound telemetry uploads Nov 12, 2026
@anton-107

Copy link
Copy Markdown
Contributor Author

Thanks for the second pass. Changes in 29d7bbc; the inline threads have the details.

Questions

  1. Scope: I'd rather keep this as one PR. I've retitled it, and the commit subject, to "aitools: log install-hint telemetry and bound telemetry uploads", so the upload change is visible in history. The upload change and its tests are all in libs/telemetry/logger.go and logger_test.go, so it can still be reverted on its own.
  2. Exit cost: A hint-only upload is now skipped when the command's credentials never resolved. The SDK sets cfg.AuthType only after authentication succeeds, so when the command's own auth failed, the hint events are dropped and nothing reruns the credential chain at exit. Uploads that include any other event behave as before. TestTelemetryUploadHintOnlyNeedsResolvedAuth covers both cases.

Outside the diff

  • Merge Danger now names the 1s budget alongside the 3s one in the U2M refresh note.
  • I kept the body format: the 16 PRs in this stack share one format, and moving them all to the template would be churn. Happy to switch if you feel strongly.

Other nits

  • within now recovers panics.
  • HasDatabricksToolingInstalled calls DetectInstalled only once.
  • logHintShown moved into hint.go.
  • Added .nextchanges/cli/apps-init-plugin-tip.md.
  • When the shim's exec fails, the second upload is now skipped while a goroutine of the first one still runs.
  • Recording the APPS_INIT_PROMPT answer is declined for this PR, since it would need a schema change; Merge Danger notes it.

@simonfaltum simonfaltum left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Automated review from simonfaltum's review agent (pr-review skill) at 29d7bbc. Each finding was verified against the code; simonfaltum may follow up.

Not approving yet: 3 open design questions.

This PR logs an install-hint telemetry event for the Claude Code hint and the apps init tip and prompt. It bounds the whole telemetry upload, including config resolution and token fetch, by its timeout, and keeps apps init quiet when an agent has the databricks plugin. CI is green, the telemetry and agents tests pass under -race, and nothing is blocking. The main issue is that the new cfg.AuthType == \"\" gate gets auth success wrong both ways. It reruns credentials for databricks auth login profiles whose auth failed, and it drops hint events from commands run inside bundle directories. There are also a few nits and three open questions: scope, whether the event is needed beyond the existing User-Agent signal, and how broad the apps init check should be.

Questions

  1. You've explained why you'd keep the hint event and the upload-bounding change in one PR, with logger.go revertible on its own. logger.go now also carries hint-specific logic (onlyHintEvents, hintOnlyUploadTimeout and the AuthType gate), so reverting the bounding change alone would also touch the hint path. AGENTS.md asks for one change per PR. Would you land the upload-bounding change (within/newAPIClient/authenticate and their tests) as its own PR under this one? Or do you see the coupling as a reason to keep them together?
  2. Every command already sends agent/claude-code plus aitools-agent/<plugin|skills|none> in its User-Agent, which installer/useragent.go describes as giving a per-calling-agent install rate. What does the new event let you measure that this signal can't? The event has no session key to join a hint to a later install, and it adds an exit-time upload of up to 1s on hinted commands (on every command when CLAUDE_CODE_SESSION_ID is unset). Would a User-Agent marker for 'hint shown' be enough, or is the separate event needed for a specific dashboard?
  3. HasDatabricksToolingInstalled treats tooling as installed when any one detected agent has the databricks plugin. With Claude Code (plugin installed) and Cursor (no skills) both detected, apps init now shows neither the tip nor the install prompt, although InstallAllSkills would set Cursor up. Before, only shared skills, which every agent uses, counted. Is hiding the recommendation for the other agents intended? Or should the check require every detected agent to have the plugin or the shared skills?

Agreeing with existing threads

  • [nice to have] In-flight U2M token refresh is abandoned at the upload deadline: #7467 (comment)
  • [nit] PR body doesn't use the repo's Changes/Why/Tests template: #7467 (review)

Comment thread libs/telemetry/logger.go Outdated
Comment on lines +132 to +137
// Hints are shown before the command authenticates, and the SDK sets
// AuthType only once authentication succeeds. When the command's own auth
// never succeeded, uploading would run the credential chain again at exit
// for events that would not otherwise be uploaded at all.
cfg := cmdctx.ConfigUsed(ctx)
if onlyHintEvents(l.logs) && cfg.AuthType == "" {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nice to have] cfg.AuthType == "" doesn't track whether the command's auth succeeded, and it errs in both directions. (1) The SDK fills AuthType from the profile or DATABRICKS_AUTH_TYPE during EnsureResolved, before authenticating, and every databricks auth login profile has auth_type = databricks-cli (cmd/auth/login.go:559). So when a hinted jobs list fails auth (revoked refresh token, VPN down), the exit-time upload still reruns the U2M strategy, up to three times within the 1s budget. I reproduced this with a databricks-cli profile and no cached token. TestTelemetryUploadHintOnlyNeedsResolvedAuth marks the config as resolved with a user-set AuthType: "pat", so it can't catch this. (2) Inside a bundle directory, MustWorkspaceClient stores a fresh b.Config.Workspace.Config(ctx) as ConfigUsed (cmd/root/auth.go:398), but authenticates a different config built by Workspace.Client, because Workspace.Config returns a new *config.Config on every call. The stored one never gets an AuthType, so hint events from jobs list and similar in bundle projects are dropped even though auth succeeded. Could you gate on a signal that is set only once the client the command uses has authenticated, for example a flag recorded in cmd/root after Authenticate returns nil, with the bundle path covered too? Please add regression cases for a preset auth type with failing credentials and for a bundle directory, and update the comment, protos/README.md and the PR description to match.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right on both counts, so I removed the gate in 2c02b39.

A signal that tracks real auth success isn't contained in this PR. The SDK has no accessor that says a config has authenticated. Bundle commands such as bundle validate and bundle deploy don't authenticate in cmd/root either: configureBundle only builds the client (its comment notes there is no network I/O), and authentication happens on the bundle's own client when the bundle phases make their first request. A flag recorded after Authenticate in MustWorkspaceClient/MustAccountClient would therefore drop hint events from every bundle command. Covering them would mean hooking the bundle client paths, which is outside this change.

With the gate gone:

  • A hinted command whose own auth failed tries the upload again at exit, and the hint-only timeout bounds that to 1s.
  • Hint events from jobs list and similar commands in a bundle directory upload again.

Both directions are tested:

  • TestTelemetryUploadHintOnlyWithFailingAuthIsBounded uses a preset AuthType: "databricks-cli" whose token source keeps failing: no request is sent and Upload gives up within 1s.
  • TestTelemetryUploadHintOnlyWithUnresolvedConfig uses a config that never authenticated, as in a bundle directory: the event uploads. It fails with the old gate.

The code comment and the PR description are updated. protos/README.md never described the gate, so it needed no change.

Comment thread libs/aitools/agents/skills.go Outdated
@@ -24,11 +24,33 @@ const (
// Returns true if no agents are detected (nothing to recommend) or if skills exist in
// ~/.databricks/aitools/skills/ or the legacy ~/.databricks/agent-skills/.
func HasDatabricksSkillsInstalled(ctx context.Context) bool {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] With apps init and RecommendSkillsInstall switched to HasDatabricksToolingInstalled, HasDatabricksSkillsInstalled is only called from skills_test.go. CI's deadcode -test won't flag a func reachable only from tests, which is the case AGENTS.md:122 describes. Please remove it and fold its test cases into TestHasDatabricksToolingInstalled (or point them at hasSharedSkills). If a later PR in the stack needs it, mark it //deadcode:allow <reason>.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed HasDatabricksSkillsInstalled in 2c02b39. No caller is left in this PR or in the PRs above it. Its test cases now target HasDatabricksToolingInstalled; their agents have no plugin reader, so they still exercise the skills-only path.

Comment thread libs/aitools/agents/skills.go Outdated
return true
}
for _, a := range installed {
if _, ok := a.DatabricksPluginVersion(ctx); ok {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] DatabricksPluginVersion returns ok for a plugin record in any scope or project. With only {"scope":"project","projectPath":"/work/other"} in installed_plugins.json, databricks apps init in /work/new skips the tip and the install prompt, although the plugin won't load for the new app. For a check about the project being created, databricksPluginActiveIn(ctx, cwd) (already used for the User-Agent) matches better. Or, if the install hint's "any trace of an install" semantics are intended here too, a short comment saying so would help.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The check now uses databricksPluginActiveIn(ctx, cwd), with the working directory resolved the same way the User-Agent code resolves it. A plugin recorded only for another project no longer counts. It also requires the plugin for every detected agent (see Q3). TestHasDatabricksToolingInstalled has a "plugin for another project only" case, which fails with DatabricksPluginVersion. In 2c02b39.


// hasSharedSkills reports whether Databricks skills are in the canonical
// skills dir or the legacy one.
func hasSharedSkills(ctx context.Context) bool {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] hasSharedSkills is the same canonical-or-legacy check that claudeToolingInstalled does inline (hint.go:117-122). Having claudeToolingInstalled call hasSharedSkills keeps the install hint and the apps init tip from drifting apart if a skills location is added.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

claudeToolingInstalled now calls hasSharedSkills, so the install hint and the apps init check read the same skills locations. In 2c02b39.

The CLI logs a new `aitools_hint_event` each time it shows a recommendation to install Databricks AI tools, so hints shown can be compared with installs that follow. It also bounds the whole telemetry upload, including client construction and authentication, by the upload timeout.

```diff
 MaybeHint(ctx, cmd)              # non-aitools command run by Claude Code without the tools
   cmdio.LogString(hintText)
+  logHintShown(INSTALL_TEXT)
 runCreate (apps init)
   flags mode, skills missing
-    cmdio.LogString("Tip: ...")
+    PrintSkillsTip                # prints the same tip
+      logHintShown(APPS_INIT_TIP)
   otherwise: RecommendSkillsInstall, skills missing
     no prompt possible
-      cmdio.LogString("Tip: ...")
+      PrintSkillsTip              # APPS_INIT_TIP
+    logHintShown(APPS_INIT_PROMPT)  # before asking, so a declined prompt counts
     AskYesOrNo(...)
```

Wire shape (one entry per hint shown):

```json
{"aitools_hint_event": {"hint_kind": "INSTALL_TEXT"}}
```

- The event carries only the hint kind. The command it was shown on is the entry's `execution_context.command`. The calling agent is not a field: the SDK already sends `agent/<name>` in the User-Agent of the telemetry upload.
- `AitoolsHintKind` also declares `CLAUDE_CODE_NATIVE` and `AGENT_INSTALL_TEXT`, which nothing emits yet.
- One `apps init` run can log two events (`INSTALL_TEXT` from the root hint and `APPS_INIT_TIP`), so count per `hint_kind`.
- The `apps init` tip and prompt now also stay silent when every detected agent has the databricks plugin for the working directory in its own plugin manifest, not only when the shared skills are installed (`HasDatabricksToolingInstalled`).

Upload timeout (`libs/telemetry/logger.go`). The SDK resolves the config (including host metadata discovery) and fetches tokens with `context.Background()`, so before this change the upload timeout bounded only the HTTP requests. A slow token endpoint or host could hold the command's exit far past the timeout.

```diff
 Upload(ctx)
-  client.New(cfg)                       # unbounded: config resolution, host metadata
   ctx = WithTimeout(uploadTimeout)      # 3s, DATABRICKS_CLI_TELEMETRY_TIMEOUT
+  within(ctx, newAPIClient(cfg))        # client construction gives up at the deadline
+    AuthVisitor = authenticate          # every request, SDK retries included:
+      within(ctx, cfg.Authenticate)     # gives up at the deadline
   for 3 attempts: attempt(ctx, ...)     # requests stay on the caller's goroutine
```

An upload whose events are all hint events uses a 1s timeout instead of the 3s default, since most commands that show a hint would otherwise not upload at all. `DATABRICKS_CLI_TELEMETRY_TIMEOUT`, when set, applies to every upload.

On timeout the goroutine started by `within` is abandoned; a panic in it is returned as an error. `cmd/root` runs `Upload` as the last step before the process exits; the ssh agent shim runs it just before exec, and while an abandoned goroutine still runs, a second `Upload` on the same logger is skipped.

Supporting changes:

```text
libs/telemetry/logger.go           BufferedEvents(ctx): read buffered events from tests in other packages
libs/testserver/handlers.go        POST /telemetry-ext accepts every log in the request
acceptance/cmd/aitools/telemetry/  print_telemetry helper + `hint` and `apps-init-tip` cases
libs/aitools/agents/hint_test.go   uses testutil.SetAgentEnv instead of its own agent env list
```

Changelog fragments: `.nextchanges/cli/telemetry-upload-timeout.md`, `.nextchanges/cli/apps-init-plugin-tip.md`.

Co-authored-by: Isaac <no-reply@databricks.com>
@anton-107
anton-107 force-pushed the aitools/install-hint-telemetry branch from 29d7bbc to 2c02b39 Compare November 12, 2026 10:23
@anton-107

Copy link
Copy Markdown
Contributor Author

Thanks for the third pass. Changes in 2c02b39; the inline threads have the details.

Questions

  1. Scope: I'd like to keep this as one PR. The hint-specific pieces in logger.go are isolated helpers: hintOnlyUploadTimeout and onlyHintEvents, used only by uploadTimeout. The AuthType gate is gone (see that thread). Reverting the bounding (within, newAPIClient, authenticate and their tests) leaves those helpers as they are.

  2. Event vs User-Agent: aitools-agent/<plugin|skills|none> reports what tooling the calling agent has on each request. It doesn't say whether the CLI showed a hint, which hint, or on which command. The event records exactly that: the hint kind (INSTALL_TEXT, APPS_INIT_TIP or APPS_INIT_PROMPT) and the command it was shown on (execution_context.command), one entry per hint printed. A later request's User-Agent can't attribute anything to the hint itself.

    The exit cost is bounded: hint-only uploads have a 1s budget, and the install hint prints at most once per Claude Code session per hour when CLAUDE_CODE_SESSION_ID is set.

  3. apps init check: Changed as you suggest. HasDatabricksToolingInstalled now treats tooling as installed only when the shared skills are present, or every detected agent has the databricks plugin for the working directory. With Claude Code (plugin) and Cursor (no skills) both detected, the tip and the prompt are shown again. TestHasDatabricksToolingInstalled has a "plugin for one of two agents" case, which fails with the any-agent check. The changelog fragment now says "every detected coding agent".

PR body template: I've kept the current format; the 16 PRs in this stack use one consistent format. Happy to switch if you feel strongly.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-auto PRs made by AI loops & factories,

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants