Skip to content

feat(appkit): tag analytics statements with query tags for warehouse attribution - #697

Open
atilafassina wants to merge 5 commits into
mainfrom
query-tags
Open

atilafassina wants to merge 5 commits into
mainfrom
query-tags

Conversation

@atilafassina

@atilafassina atilafassina commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Analytics statements now carry query tags. Warehouse owners can then attribute load and latency in system.query.history.query_tags to the app, query and page that caused it, instead of pattern-matching SQL text.

  • Connector pass-through (bug fix): SQLWarehouseConnector.executeStatement rebuilt the request body field by field and dropped queryTags. It now forwards them.
  • Automatic tags: statements from /query (JSON and Arrow), /metric and the query agent tool are tagged appkit_app (DATABRICKS_APP_NAME), appkit_query_key, appkit_route (query / metric / agent_tool) and appkit_lane (sp / obo). Opt out with autoQueryTags: false.
  • Custom tags: analytics({ queryTags }) accepts a static map or a function evaluated per request ({ route, queryKey, lane, req, clientTags }). A function's return value is the whole custom set: client tags are only sent if it returns them, so a client can't fill in a key the function left out (e.g. a chargeback tenant).
  • Client tags: useAnalyticsQuery and useMetricView take a queryTags option. One query can serve several pages, so each page can tag its own requests. Under no config or a static map they're merged in (config wins on collisions); under a function they're exposed as ctx.clientTags. Turn them off with clientQueryTags: false.

Rules

  • Precedence on collisions: automatic appkit_* tags, then config, then client. The appkit_ prefix is reserved.
  • The set is capped at the warehouse's 20-tag limit, with automatic tags first (merged in an insertion-ordered Map, so integer-like client keys can't jump the queue).
  • Invalid tags are dropped and never fail the query. Keys can't contain , : - / = . or control characters, have padding, or use the appkit_ prefix in any case. Keys and values are limited to 128 characters; values are truncated by code point. At most 64 client entries are examined per request, and only drop counts are logged.
  • The user's identity is never tagged. Tags are stored as plain text, and OBO statements already run as the user.
  • Tags are not part of any cache key. Cache hits don't reach the warehouse, so nothing is recorded for them, and concurrent identical requests share one in-flight statement carrying the first caller's tags.

Not in scope

  • Direct analytics.query() calls don't get automatic tags. Callers can pass { queryTags } explicitly.

Test plan

  • Unit tests: tag sanitization, merging, precedence, the 20-tag cap and the executor wrapper (query-tags.test.ts)
  • Route tests: JSON, Arrow, OBO lane, spoofed reserved keys, opt-out, agent tool, metric route
  • appkit-ui hook tests: body serialization, no refetch for equal tags, separate requests for different tags
  • pnpm build, pnpm docs:build, pnpm check:fix, pnpm -r typecheck
  • Live check (e2-dogfood staging, playground on this branch): /query JSON + Arrow, /metric, and an OBO /query all recorded the expected appkit_* tags plus client page/run_id in the query history API and system.query.history. A spoofed appkit_query_key was overridden and bad.key was dropped.
  • Reviewed by two independent agents (Codex, Claude); all findings addressed in bc073fa3

Platform observations (not fixable in AppKit)

  • The statements API does not reject invalid tags. A raw bad.key is recorded as a tags_invalid=bad.key sentinel instead, which is why AppKit drops invalid keys itself.
  • In 2 of ~17 raw probe statements, query history recorded no tags at all, even for a known-good control tag. Repeating the same tag values later recorded fine, so it looks intermittent on the platform side rather than tied to the input.

This pull request and its description were written by Isaac.

atilafassina and others added 4 commits October 28, 2026 18:14
The SQL warehouse connector rebuilt the request body field by field and
dropped queryTags, so tags set by callers never reached the warehouse.

Co-authored-by: Isaac <no-reply@databricks.com>
Signed-off-by: Atila Fassina <atila@fassina.eu>
Statements issued by the /query and /metric routes and the query agent
tool now carry query tags, recorded in system.query.history.query_tags:

- automatic appkit_app, appkit_query_key, appkit_route and appkit_lane
  tags (opt out with autoQueryTags: false)
- custom tags from the queryTags config, static or derived per request
- client-sent tags from the request body (opt out with
  clientQueryTags: false)

Automatic tags reserve the appkit_ prefix and win on collisions, then
config, then client tags; the set is capped at the warehouse's 20-tag
limit. Invalid tags are dropped instead of failing the query.

Co-authored-by: Isaac <no-reply@databricks.com>
Signed-off-by: Atila Fassina <atila@fassina.eu>
A new queryTags option forwards per-request tags (e.g. the page issuing
the query) to the analytics routes, so the same query can be attributed
to the different pages that run it. Requests with different tags are
not deduplicated together.

Co-authored-by: Isaac <no-reply@databricks.com>
Signed-off-by: Atila Fassina <atila@fassina.eu>
Co-authored-by: Isaac <no-reply@databricks.com>
Signed-off-by: Atila Fassina <atila@fassina.eu>
@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

📦 Bundle size report

Compared against bundle-size-baseline.json (main).

@databricks/appkit

npm tarball (packed): 1.2 MB (+7.5 KB) — gzipped download (dist + bin; excludes release-only docs/NOTICE).

dist raw gzip
JS (runtime) 1.3 MB (+5.9 KB) 460 KB (+2.4 KB)
Type declarations 466 KB (+2.8 KB) 169 KB (+1.4 KB)
Source maps 2.5 MB (+13 KB) 861 KB (+5.1 KB)
Other 11 KB 3.7 KB
Total 4.3 MB (+22 KB) 1.5 MB (+8.8 KB)
Per-entry composition (own code — deps external (as shipped))
Entry Initial (gz) Lazy (gz) Total (gz) node_modules (min) Own code (min)
. 107 KB (+909 B) 2.5 KB (+1 B) 109 KB (+910 B) external 346 KB (+2.3 KB)
./beta 103 KB (+8 B) 485 B 103 KB (+8 B) external 309 KB (+22 B)
./testing 45 KB (+1 B) 32 KB (-3 B) 77 KB (-2 B) external 223 KB
./tsdown 520 B 0 B 520 B external 813 B
./type-generator 26 KB 0 B 26 KB external 74 KB

Chunks:

Entry Chunk Load Size (gz)
. index.js initial 102 KB
. utils.js initial 4.6 KB
. remote-tunnel-manager.js lazy 2.5 KB
./beta beta.js initial 82 KB
./beta modular.js initial 6.0 KB
./beta stream-manager.js initial 5.9 KB
./beta service-context.js initial 4.2 KB
./beta databricks.js initial 3.3 KB
./beta client.js initial 647 B
./beta index.js initial 20 B
./beta supervisor-api.js lazy 192 B
./beta databricks.js lazy 178 B
./beta index.js lazy 115 B
./testing manifest.js initial 32 KB
./testing index.js initial 10 KB
./testing wide-event-emitter.js initial 2.9 KB
./testing index.js lazy 28 KB
./testing remote-tunnel-manager.js lazy 2.5 KB
./testing utils.js lazy 1.8 KB
./tsdown index.js initial 520 B
./type-generator index.js initial 26 KB

@databricks/appkit-ui

npm tarball (packed): 398 KB (+428 B) — gzipped download (dist + bin; excludes release-only docs/NOTICE).

dist raw gzip
JS (runtime) 429 KB (+206 B) 147 KB (+43 B)
Type declarations 268 KB (+842 B) 99 KB (+298 B)
Source maps 853 KB (+452 B) 286 KB (+100 B)
CSS 16 KB 3.2 KB
Total 1.5 MB (+1.5 KB) 534 KB (+441 B)
Per-entry composition (consumer bundle — deps bundled, peerDeps external)
Entry Initial (gz) Lazy (gz) Total (gz) node_modules (min) Own code (min)
./js 5.3 KB 49 KB 55 KB 208 KB 14 KB
./js/beta 2.1 KB 0 B 2.1 KB 0 B 5.0 KB
./react 433 KB (+59 B) 49 KB 481 KB (+59 B) 1.3 MB (+3 B) 178 KB (+91 B)
./react/beta 4.9 KB 0 B 4.9 KB 0 B 12 KB

Chunks:

Entry Chunk Load Size (gz)
./js index.js initial 5.2 KB
./js chunk initial 120 B
./js apache-arrow lazy 49 KB
./js/beta beta.js initial 2.1 KB
./react index.js initial 431 KB
./react tslib initial 2.1 KB
./react apache-arrow lazy 49 KB
./react/beta beta.js initial 4.9 KB

@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

🤖 AppKit PR bot

🔬 Run evals

Start an eval for this PR from the evals-monitor app: Go to Evals Monitor →

📦 Try this PR's app template

Scaffolds a new app from this PR's SDK build. Run it in any folder (requires the GitHub CLI — gh auth login — and the Databricks CLI):

gh run download 40877534105 -R databricks/appkit -n appkit-template-0.89.0-pr.af1ab41-query-tags-697 -D appkit-pr-697 \
  && unzip -o "appkit-pr-697/appkit-template-0.89.0-pr.af1ab41-query-tags-697.zip" -d "appkit-pr-697" \
  && databricks apps init --template "appkit-pr-697"

The template pins @databricks/appkit and @databricks/appkit-ui to tarballs built from this branch, so the scaffolded app runs against this PR's code.

- A function queryTags config now owns the custom tag set: client tags
  are only sent if it returns them (ctx.clientTags). Previously client
  tags were always merged in, so a conditional function (e.g. tenant from
  a header) let any client forge the key it left out.
- Merge tags in an insertion-ordered Map. A plain object enumerated
  integer-like client keys first, so 20 of them pushed every appkit_*
  tag past the 20-tag cap; it also swallowed __proto__ tags.
- Reject control characters, padded keys and the appkit_ prefix in any
  case; truncate values by code point.
- Examine at most 64 client entries and log only dropped counts, never
  untrusted key names.
- Sanitize a static queryTags map once instead of warning per statement.
- Export QueryTagMap and QueryTagsContext; document that concurrent
  identical requests share the first caller's tags.

Co-authored-by: Isaac <no-reply@databricks.com>
Signed-off-by: Atila Fassina <atila@fassina.eu>
@atilafassina
atilafassina marked this pull request as ready for review October 29, 2026 14:29
@atilafassina
atilafassina requested a review from a team as a code owner October 29, 2026 14:29
@atilafassina
atilafassina requested review from MarioCadenas and a balanced review from Copilot October 29, 2026 14:29

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Approval recommended

No active review issues are reported for the query-tag attribution changes.

No open issues 🎉

What changed in this PR

Adds query-tag attribution to AppKit analytics so warehouse owners can identify the app, query, route, and execution lane behind statements in system.query.history.

Changes:

  • Forwards query tags through the SQL warehouse connector and adds automatic tags to analytics routes and agent-tool queries.
  • Supports server-configured and client-supplied tags with validation, precedence rules, opt-outs, and a 20-tag limit.
  • Adds React hook options, regression tests, and documentation for tagging and cache behavior.
File Description
packages/​shared/​src/​workspace-client/​modular.ts Exports the SDK QueryTag type.
packages/​appkit/​src/​plugins/​analytics/​types.ts Defines tag configuration and request options.
packages/​appkit/​src/​plugins/​analytics/​tests/​query-tags.test.ts Tests validation, precedence, limits, and executor wrapping.
packages/​appkit/​src/​plugins/​analytics/​tests/​metric.test.ts Tests metric statement attribution.
packages/​appkit/​src/​plugins/​analytics/​tests/​analytics.test.ts Tests route, fallback, OBO, and agent-tool tags.
packages/​appkit/​src/​plugins/​analytics/​tests/​analytics.readonly.test.ts Updates agent-tool expectations for automatic tags.
packages/​appkit/​src/​plugins/​analytics/​result-delivery.ts Allows tags in executor format parameters.
packages/​appkit/​src/​plugins/​analytics/​query-tags.ts Implements tag sanitization, merging, and forwarding.
packages/​appkit/​src/​plugins/​analytics/​mv/​schemas.ts Accepts client tags for lenient validation.
packages/​appkit/​src/​plugins/​analytics/​manifest.json Declares automatic and client-tag toggles.
packages/​appkit/​src/​plugins/​analytics/​analytics.ts Applies tags to query, metric, and agent-tool execution.
packages/​appkit/​src/​connectors/​tests/​sql-warehouse.test.ts Tests connector tag pass-through.
packages/​appkit/​src/​connectors/​sql-warehouse/​client.ts Includes tags in statement requests.
packages/​appkit-ui/​src/​react/​hooks/​use-metric-view.ts Serializes metric query tags.
packages/​appkit-ui/​src/​react/​hooks/​use-analytics-query.ts Stabilizes and serializes analytics query tags.
packages/​appkit-ui/​src/​react/​hooks/​types.ts Adds public tag types and hook options.
packages/​appkit-ui/​src/​react/​hooks/​index.ts Exports AnalyticsQueryTags.
packages/​appkit-ui/​src/​react/​hooks/​__tests__/​use-metric-view.test.ts Tests metric tag serialization.
packages/​appkit-ui/​src/​react/​hooks/​__tests__/​use-analytics-query.test.ts Tests serialization and tag-aware request sharing.
docs/​docs/​plugins/​analytics.md Documents configuration, validation, attribution, and caching.

Review effort: Balanced

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

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants