diff --git a/docs/api-reference/beta/openapi.json b/docs/api-reference/beta/openapi.json index 8e489f9a5..639396f96 100644 --- a/docs/api-reference/beta/openapi.json +++ b/docs/api-reference/beta/openapi.json @@ -1,6 +1,60 @@ { "components": { "schemas": { + "AggregatedHealthScore": { + "properties": { + "dimensions": { + "description": "Latest aggregated score per dimension. Dimensions with no results in the last 7 days are omitted.", + "items": { + "$ref": "#/components/schemas/DimensionHealthScore" + }, + "title": "Dimensions", + "type": "array" + }, + "matched_count": { + "description": "Number of table assets you can view that match the filters, whether or not they have test results in the window. `0` when nothing matches.", + "title": "Matched Count", + "type": "integer" + }, + "total": { + "$ref": "#/components/schemas/AggregatedHealthScoreBucket", + "description": "Weighted score across all dimensions for the whole matching asset set, using the environment's configured dimension weights. Aggregated across the assets' test results, not an average of per-asset scores." + } + }, + "required": ["matched_count", "total", "dimensions"], + "title": "AggregatedHealthScore", + "type": "object" + }, + "AggregatedHealthScoreBucket": { + "properties": { + "bucket_at": { + "anyOf": [ + { + "format": "date-time", + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Start of the hourly bucket this score was computed for. `null` when no matching asset has test results in the window.", + "title": "Bucket At" + }, + "score": { + "description": "Health score in the range 0-1 for the most recent hourly bucket. `0` when no matching asset has test results in the window.", + "title": "Score", + "type": "number" + }, + "test_count": { + "description": "Number of test results that contributed to this score.", + "title": "Test Count", + "type": "integer" + } + }, + "required": ["score", "test_count", "bucket_at"], + "title": "AggregatedHealthScoreBucket", + "type": "object" + }, "ApiError": { "properties": { "code": { @@ -1545,7 +1599,7 @@ }, "/public/beta/{env_id}/asset-health-scores": { "get": { - "description": "Lists the current health score of every asset that has test results in the last 7 days \u2014 a full snapshot, recomputed hourly. Not an incremental feed: re-read the snapshot instead of tracking changes. Ordered by `asset_id`.", + "description": "Lists the current health score of every table asset that has test results in the last 7 days \u2014 a full snapshot, recomputed hourly. Not an incremental feed: re-read the snapshot instead of tracking changes. Ordered by `asset_id`.", "operationId": "list_asset_health_scores_public_beta__env_id__asset_health_scores_get", "parameters": [ { @@ -1726,9 +1780,292 @@ "tags": ["Asset health scores"] } }, + "/public/beta/{env_id}/asset-health-scores/aggregate": { + "get": { + "description": "Returns one health score card for a set of table assets \u2014 the same number the Data Health dashboard shows for a filter or folder. The `ids`, `source_types`, `tags`, `db_names`, `schema_names`, `materializations` and `path` filters intersect; with no filters, every table asset you can view is included. Every `ids` value must be a table asset, otherwise the request fails with 400 listing the invalid ids. The score is aggregated across the matching assets' test results in the last 7 days, not averaged from per-asset scores. When no matching asset has test results in the window, returns a zero card (`score` 0, `test_count` 0, `bucket_at` null, no dimensions) alongside `matched_count`.", + "operationId": "aggregate_asset_health_scores_public_beta__env_id__asset_health_scores_aggregate_get", + "parameters": [ + { + "description": "Environment identifier that scopes the request.", + "in": "path", + "name": "env_id", + "required": true, + "schema": { + "description": "Environment identifier that scopes the request.", + "title": "Env Id", + "type": "string" + } + }, + { + "description": "Aggregate only table assets with these ids.", + "in": "query", + "name": "ids", + "required": false, + "schema": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "maxItems": 1000, + "type": "array" + }, + { + "type": "null" + } + ], + "description": "Aggregate only table assets with these ids.", + "title": "Ids" + } + }, + { + "description": "Aggregate only table assets from these source types (e.g. `dbt`, `fivetran`).", + "in": "query", + "name": "source_types", + "required": false, + "schema": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "maxItems": 1000, + "type": "array" + }, + { + "type": "null" + } + ], + "description": "Aggregate only table assets from these source types (e.g. `dbt`, `fivetran`).", + "title": "Source Types" + } + }, + { + "description": "Aggregate only table assets tagged with any of these tags.", + "in": "query", + "name": "tags", + "required": false, + "schema": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "maxItems": 1000, + "type": "array" + }, + { + "type": "null" + } + ], + "description": "Aggregate only table assets tagged with any of these tags.", + "title": "Tags" + } + }, + { + "description": "Aggregate only table assets in these databases.", + "in": "query", + "name": "db_names", + "required": false, + "schema": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "maxItems": 1000, + "type": "array" + }, + { + "type": "null" + } + ], + "description": "Aggregate only table assets in these databases.", + "title": "Db Names" + } + }, + { + "description": "Aggregate only table assets in these schemas.", + "in": "query", + "name": "schema_names", + "required": false, + "schema": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "maxItems": 1000, + "type": "array" + }, + { + "type": "null" + } + ], + "description": "Aggregate only table assets in these schemas.", + "title": "Schema Names" + } + }, + { + "description": "Aggregate only table assets with these materializations (e.g. `table`, `view`, `incremental`).", + "in": "query", + "name": "materializations", + "required": false, + "schema": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "maxItems": 1000, + "type": "array" + }, + { + "type": "null" + } + ], + "description": "Aggregate only table assets with these materializations (e.g. `table`, `view`, `incremental`).", + "title": "Materializations" + } + }, + { + "description": "Return only table assets whose `path` equals one of these paths or is nested under one of them (`/`-separated, e.g. `models/marts` matches `models/marts/orders.sql`; a trailing `/` is ignored). Repeatable; assets without a `path` never match.", + "in": "query", + "name": "path", + "required": false, + "schema": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "maxItems": 1000, + "type": "array" + }, + { + "type": "null" + } + ], + "description": "Return only table assets whose `path` equals one of these paths or is nested under one of them (`/`-separated, e.g. `models/marts` matches `models/marts/orders.sql`; a trailing `/` is ignored). Repeatable; assets without a `path` never match.", + "title": "Path" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AggregatedHealthScore" + } + } + }, + "description": "Successful Response" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + }, + "description": "Invalid cursor or request parameters." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + }, + "description": "Authentication is required." + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + }, + "description": "The token does not have permission to access the resource." + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + }, + "description": "The requested resource was not found." + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + }, + "description": "Request validation failed." + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + }, + "description": "Rate limit exceeded.", + "headers": { + "RateLimit-Limit": { + "description": "Maximum requests allowed in the current window.", + "schema": { + "type": "integer" + } + }, + "RateLimit-Remaining": { + "description": "Requests remaining in the current window.", + "schema": { + "type": "integer" + } + }, + "RateLimit-Reset": { + "description": "Seconds until the current rate-limit window resets.", + "schema": { + "type": "integer" + } + }, + "Retry-After": { + "description": "Seconds to wait before retrying.", + "schema": { + "type": "integer" + } + } + } + }, + "500": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiErrorResponse" + } + } + }, + "description": "An unexpected server error occurred." + } + }, + "summary": "Aggregate asset health scores", + "tags": ["Asset health scores"] + } + }, "/public/beta/{env_id}/asset-health-scores/{asset_id}": { "get": { - "description": "Fetches the current health score of a single asset. Returns 404 if the asset doesn't exist in this environment or has no test results in the last 7 days.", + "description": "Fetches the current health score of a single table asset. Returns 404 if the id isn't a table asset in this environment or has no test results in the last 7 days.", "operationId": "get_asset_health_score_public_beta__env_id__asset_health_scores__asset_id__get", "parameters": [ { @@ -2131,6 +2468,28 @@ "title": "Tags" } }, + { + "description": "Return only assets whose `path` equals one of these paths or is nested under one of them (`/`-separated, e.g. `models/marts` matches `models/marts/orders.sql`; a trailing `/` is ignored). Repeatable; assets without a `path` never match.", + "in": "query", + "name": "path", + "required": false, + "schema": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "maxItems": 1000, + "type": "array" + }, + { + "type": "null" + } + ], + "description": "Return only assets whose `path` equals one of these paths or is nested under one of them (`/`-separated, e.g. `models/marts` matches `models/marts/orders.sql`; a trailing `/` is ignored). Repeatable; assets without a `path` never match.", + "title": "Path" + } + }, { "description": "Upserts feed: assets whose `synced_at` >= this time (inclusive). Orders by `synced_at`. Clamped to the last 90 days \u2014 an older time is treated as 90 days ago.", "in": "query", @@ -2440,6 +2799,28 @@ "title": "Bi Platforms" } }, + { + "description": "Return only BI assets whose `path` equals one of these paths or is nested under one of them (`/`-separated, e.g. `models/marts` matches `models/marts/orders.sql`; a trailing `/` is ignored). Repeatable; assets without a `path` never match.", + "in": "query", + "name": "path", + "required": false, + "schema": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "maxItems": 1000, + "type": "array" + }, + { + "type": "null" + } + ], + "description": "Return only BI assets whose `path` equals one of these paths or is nested under one of them (`/`-separated, e.g. `models/marts` matches `models/marts/orders.sql`; a trailing `/` is ignored). Repeatable; assets without a `path` never match.", + "title": "Path" + } + }, { "description": "Upserts feed: assets whose `synced_at` >= this time (inclusive). Orders by `synced_at`. Clamped to the last 90 days \u2014 an older time is treated as 90 days ago.", "in": "query", @@ -2933,6 +3314,28 @@ "title": "Materializations" } }, + { + "description": "Return only table assets whose `path` equals one of these paths or is nested under one of them (`/`-separated, e.g. `models/marts` matches `models/marts/orders.sql`; a trailing `/` is ignored). Repeatable; assets without a `path` never match.", + "in": "query", + "name": "path", + "required": false, + "schema": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "maxItems": 1000, + "type": "array" + }, + { + "type": "null" + } + ], + "description": "Return only table assets whose `path` equals one of these paths or is nested under one of them (`/`-separated, e.g. `models/marts` matches `models/marts/orders.sql`; a trailing `/` is ignored). Repeatable; assets without a `path` never match.", + "title": "Path" + } + }, { "description": "Upserts feed: assets whose `synced_at` >= this time (inclusive). Orders by `synced_at`. Clamped to the last 90 days \u2014 an older time is treated as 90 days ago.", "in": "query", diff --git a/docs/api/introduction.mdx b/docs/api/introduction.mdx index 00c145a27..511e459a2 100644 --- a/docs/api/introduction.mdx +++ b/docs/api/introduction.mdx @@ -65,7 +65,8 @@ Follow `next_cursor` until `has_more` is `false` — see [Pagination](/api/pagin | Tests | `GET /{env_id}/tests` | Test definitions | | Latest test executions | `GET /{env_id}/latest-test-executions` | Current run per sub-test | | Test execution history | `GET /{env_id}/tests/{test_id}/executions` | History for one test | -| Asset health scores | `GET /{env_id}/asset-health-scores` | Current health score per asset | +| Asset health scores | `GET /{env_id}/asset-health-scores` | Current health score per table asset | +| Aggregate health score | `GET /{env_id}/asset-health-scores/aggregate` | One health score for a filtered set of tables | Every list endpoint supports a full scan and keyset pagination. Assets, columns, and tests also expose incremental feeds — see @@ -89,6 +90,10 @@ fields but each kind also has its own attributes, so the API splits them: `materialization`) for tables, and `bi_platform` / `bi_type` / `url` for BI assets. Kind-specific filters live here too (e.g. `db_names` on `/assets/tables`, `bi_platforms` on `/assets/bi`). +- All three accept a **`path`** filter to select a folder: it matches assets + whose `path` equals the value or is nested under it (`models/marts` matches + `models/marts/orders.sql` but not `models/marts_legacy/orders.sql`). A + trailing `/` is ignored; repeat the parameter to combine folders. **Why the split?** A single asset shape would leave most fields null on any given row (a BI dashboard has no `db_name`; a table has no `bi_platform`). Keeping the @@ -121,7 +126,7 @@ verdict are not in this cut. ## Asset health scores **[`GET /asset-health-scores`](/api/reference/asset-health-scores/list-asset-health-scores)** -returns the current data health score of every monitored asset, the same number +returns the current data health score of every monitored table asset, the same number shown in the Elementary data health dashboard when looking at a single asset. Use it to feed scorecards or nightly quality reports. @@ -145,11 +150,65 @@ Each item has a `total` score and one entry per quality `dimension` buckets; `bucket_at` is the start of the most recent bucket with results. Dimensions with no results in that window are omitted, and assets with no results at all are not returned. +- Only [table assets](/api/reference/assets/list-table-assets) have health + scores. `GET /asset-health-scores/{asset_id}` returns `404` for a BI asset or + an id that is not a synced table. - Full snapshot, replaced in place — re-read it; there is no incremental feed. The only filter is `asset_ids`. Join to [assets](/api/reference/assets/list-assets) on `asset_id`. - `dimension` is an extensible enum (see below). +### Aggregate health score for a set of assets + +**[`GET /asset-health-scores/aggregate`](/api/reference/asset-health-scores/aggregate-asset-health-scores)** +returns a single health score for a group of table assets — for example a dbt +folder, a schema, or everything with a tag — the same number the Elementary +data health dashboard shows when you filter it to that group. It is computed +over the matching assets' test results together, **not** as an average of +their per-asset scores. + +```bash +curl -G "https://prod.api.elementary-data.com/public/beta/$ENV_ID/asset-health-scores/aggregate" \ + -H "Authorization: Bearer $ELEMENTARY_TOKEN" \ + --data-urlencode "path=models/marts" \ + --data-urlencode "tags=finance" +``` + +```json +{ + "matched_count": 40, + "total": { "score": 0.91, "test_count": 12, "bucket_at": "2026-09-14T08:00:00" }, + "dimensions": [ + { "dimension": "freshness", "score": 0.99, "test_count": 3, "bucket_at": "2026-09-14T08:00:00" } + ] +} +``` + +- `matched_count` is the number of table assets you can view that match the + filters, including those with no test results in the window. +- Filters select [table assets](/api/reference/assets/list-table-assets). The + supported filters are `ids`, `source_types`, `tags`, `db_names`, + `schema_names`, `materializations`, and `path`. Values within one parameter + are OR-ed; different parameters are AND-ed. With no filters, the call covers + every table asset in the environment that you can view. +- `total` and `dimensions` are computed only from matching tables that have test + results in the window; tables without results count toward `matched_count` + but not toward the score. +- Every `ids` value must be a table asset; if any is a BI asset or unknown, the + request fails with `400` and the error message lists the invalid ids. +- `path` selects a folder: it matches assets whose `path` equals the value or is + nested under it (`models/marts` matches `models/marts/orders.sql` but not + `models/marts_legacy/orders.sql`). A trailing `/` is ignored, and `.sql` is + not stripped. Repeat it to combine folders. The same + `path` filter is also available on `GET /assets`, `/assets/tables`, and + `/assets/bi`. +- Same scoring as above: scores are in the range `0`–`1`, computed from test + results in a fixed **last 7 days** window, and dimensions with no results are + omitted. +- If no matching asset has test results in the window, the response is still + `200` with a zero score and the `matched_count` (`0` for an empty folder): + `{"matched_count": 0, "total": {"score": 0, "test_count": 0, "bucket_at": null}, "dimensions": []}`. + ## Forward compatibility Some string fields are **extensible enums**: they carry a value from a small, diff --git a/docs/api/reference/asset-health-scores/aggregate-asset-health-scores.mdx b/docs/api/reference/asset-health-scores/aggregate-asset-health-scores.mdx new file mode 100644 index 000000000..9931d2cde --- /dev/null +++ b/docs/api/reference/asset-health-scores/aggregate-asset-health-scores.mdx @@ -0,0 +1,3 @@ +--- +openapi: api-reference/beta/openapi.json get /public/beta/{env_id}/asset-health-scores/aggregate +--- diff --git a/docs/docs.json b/docs/docs.json index f48208af3..ab57fdda4 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -583,7 +583,8 @@ "group": "Asset health scores", "pages": [ "api/reference/asset-health-scores/list-asset-health-scores", - "api/reference/asset-health-scores/get-an-asset-health-score" + "api/reference/asset-health-scores/get-an-asset-health-score", + "api/reference/asset-health-scores/aggregate-asset-health-scores" ] } ]