Repository navigation
feat(appkit): tag analytics statements with query tags for warehouse attribution - #697
atilafassina wants to merge 5 commits into
Conversation
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>
📦 Bundle size reportCompared against
|
| 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 |
🤖 AppKit PR bot🔬 Run evalsStart an eval for this PR from the evals-monitor app: Go to Evals Monitor → 📦 Try this PR's app templateScaffolds a new app from this PR's SDK build. Run it in any folder (requires the GitHub 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 |
- 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>
There was a problem hiding this comment.
🟢 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
Summary
Analytics statements now carry query tags. Warehouse owners can then attribute load and latency in
system.query.history.query_tagsto the app, query and page that caused it, instead of pattern-matching SQL text.SQLWarehouseConnector.executeStatementrebuilt the request body field by field and droppedqueryTags. It now forwards them./query(JSON and Arrow),/metricand thequeryagent tool are taggedappkit_app(DATABRICKS_APP_NAME),appkit_query_key,appkit_route(query/metric/agent_tool) andappkit_lane(sp/obo). Opt out withautoQueryTags: false.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 chargebacktenant).useAnalyticsQueryanduseMetricViewtake aqueryTagsoption. 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 asctx.clientTags. Turn them off withclientQueryTags: false.Rules
appkit_*tags, then config, then client. Theappkit_prefix is reserved.Map, so integer-like client keys can't jump the queue)., : - / = .or control characters, have padding, or use theappkit_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.Not in scope
analytics.query()calls don't get automatic tags. Callers can pass{ queryTags }explicitly.Test plan
query-tags.test.ts)pnpm build,pnpm docs:build,pnpm check:fix,pnpm -r typecheck/queryJSON + Arrow,/metric, and an OBO/queryall recorded the expectedappkit_*tags plus clientpage/run_idin the query history API andsystem.query.history. A spoofedappkit_query_keywas overridden andbad.keywas dropped.bc073fa3Platform observations (not fixable in AppKit)
bad.keyis recorded as atags_invalid=bad.keysentinel instead, which is why AppKit drops invalid keys itself.This pull request and its description were written by Isaac.