Repository navigation
v0.9.15
Release v0.9.15
📦 NPM packages
🐳 Docker image
docker pull ms2data/malloy-publisher:0.9.15
docker pull ms2data/malloy-publisher:latest
ℹ️ Details
- Release branch:
release/v0.9.15 - Triggered by: @kylenesbit
Tiles can be placed anywhere on the grid, have heights, and are laid out on their own entries
A tile's entry in tiles=[…] can now carry its layout: "overview -> kpis" { colspan=12 rowspan=2 }. colspan and break there win over the view's # colspan and # break (and break=false turns a view's break off), so one view can sit at a different width on each report, and a tile on a view the report imports can be resized at all. rowspan=N is new: the tile's height in grid rows of 80px, plus the 16px gaps between them. A chart fills it, a table scrolls in it, and a tile keeps its height beside a taller neighbour instead of stretching to it. Without one a tile is as tall as its result, as before. The manifest's tiles carry rowspan, and the lint now reads an entry's colspan, break and rowspan instead of calling them unread.
Tiles can now be placed: row=N col=N on an entry put its top left corner at that grid cell. A report with any row is placed, so its rows are fixed 80px tracks, tiles sit exactly where they say, and empty space can hold a tile (one can sit under a short neighbour beside a tall one). A placed tile with no rowspan is 5 rows (a text tile 2); phones stack tiles top to bottom, then left to right. The lint names a placed tile past the grid's right edge and two placed tiles that cover each other. In the Console builder a move on a grid is now free placement: the first move places every tile where it is drawn, the dragged tile snaps to the cell under it, and anything it lands on is pushed down (a resize pushes the same way).
In the Console builder each tile has a second grip, on its bottom edge, that sets its height in whole rows (arrow keys step it; Delete or a double-click fits the result again). Resizing or moving a tile now writes its layout on its entry and takes the view's # colspan and # break off, so the layout has one home; an untouched tile's tags are left as written.
Server telemetry: errors logged once by status, new core metrics and trace spans
Operators who alert on Publisher's logs or metrics should read this; nothing in the REST or MCP contract changed.
Logs. Every REST error is now logged once, by status: a 4xx at warn as Request refused with the path and status, a 5xx at error. Before, most routes logged every failure at error, a 5xx twice, and the materialization routes logged nothing. Errors passed through the error middleware (the concurrency cap's 503, a malformed JSON body's 400) follow the same rule instead of Unhandled error: at error. The MCP endpoint no longer writes five info lines per request; each tool call logs one MCP tool call line with the tool, outcome and duration, and transport detail is at debug. execute_query no longer logs its parameters, which include given values, at info. Serving query from storage tier moved to debug (the routing counter already carries it), Query parsing error to warn, and the query-path log field that said environmentName but held the package name is now packageName. An alert keyed on any of these lines needs updating.
Metrics. malloy_model_query_duration{malloy.model.query.status} gains a third value, rejected, for a request malformed before anything compiled. Those were counted as error, so an error-rate panel built on this histogram will read lower for the same traffic; add rejected to it if you want the old total. The OpenTelemetry resource now reports the real service.version instead of 1.0.0. New: publisher_mcp_tool_calls_total and publisher_mcp_tool_duration_ms, publisher_compiles_total and publisher_compile_duration_ms, publisher_packages{state} (serving / failed / stale), publisher_package_reloads_total{trigger,outcome}, publisher_connection_operations_total, publisher_mcp_row_limit_hits_total, and publisher_storage_bindings_withheld_total. The default OpenAI-compatible embedding provider now records the publisher_llm_* metrics the other providers already did. docs/configuration.md describes each.
Traces. With OTEL_EXPORTER_OTLP_ENDPOINT set, queries are traced as a publisher.query span with publisher.query.execute beneath it, and compiles as publisher.compile, nested under the HTTP spans.
Embedding provider errors. Three failure paths of the default OpenAI-compatible embedding client now behave like the other providers: a non-auth HTTP error reports the vendor's parsed message rather than a raw 200-character body excerpt, a non-JSON reply fails without retrying, and a connection dropped while reading the body is retried.
The builder's filter row is in the same order as the reader's
The report builder drew its filter row in a different order from the report it edits. It put the file's own filters first and the model's after them. The reader lists a filter when a tile first reads it, going through the tiles in order, and within each tile it lists what the tile's source reads before the tile's own where: bindings. So a report whose model filters every tile by Manufacturer read "Manufacturer, Subject, Recall date" in the view and "Subject, Recall date, Manufacturer" in the builder. The builder now uses the reader's rule, so both show the same row. A filter you add lands where the saved report will put it: after the filters its tile already has. A host that draws its own row for a report should take the manifest's givens in the order given, not regroup them by where each given is declared.
The builder says "model" and "tile", not "view"
The builder's text no longer assumes the reader knows tiles run Malloy views. In the chart menu, "From the view" is now Default, with "As set in the model" under it. The reasons on greyed-out charts no longer say "view" either, and nor does the text for adding a tile or the New report dialog's. A host's tests that look up these labels or roles by name need the new strings: Default, Search the model, and the From the model listbox.
A filter's menu now speaks of the report's default as well. "Open the report with this value" becomes Make this the report's default. Once it is, the menu shows a quiet Report default with a check. The button that clears it says Use the model's default for a model's filter, or Use the filter's default for one the report declares itself, since that one falls back to the default written in the report.
What's Changed
- chore(release): 0.9.14 by @kylenesbit in #1437
- build(docker): keep dependency layers unchanged across releases by @housejester in #1425
- fix(storage): don't log the catalog password for a never-built destination by @housejester in #1441
- fix(eval-loop): launch Publisher with node, not bun by @Sha-Bang in #1442
- fix(builder): filter row order matches the reader; clearer builder copy by @kylenesbit in #1447
- refactor(server): quality pass — simplify, test pyramid, telemetry, comments by @kylenesbit in #1448
- fix(builder): open named-query tiles, offer only charts a view can draw, never clip a tile, and add bar-and-line charts by @jswir in #1424
- fix(server): a Snowflake attached database writes only the fields that are set by @kylenesbit in #1450
- fix(server): escape quotes the BigQuery way in incremental refresh SQL by @kylenesbit in #1451
- fix(server): read a raw Azure URL only inside a configured attachment by @kylenesbit in #1452
- fix(server): clear the Snowflake query tag after a storage build by @kylenesbit in #1449
- feat(reports): free tile placement (row/col), heights (rowspan), and layout on each tile's entry by @kylenesbit in #1453
- test(server): give three timing-sensitive tests room on Windows CI by @kylenesbit in #1454
Full Changelog: v0.9.14...v0.9.15