Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
# Changelog

## 1.0.1

## 1.1.0

- **New: `metric data`** loads a metric's values over a date range (`--from`, `--to`, `--granularity daily|weekly|monthly|quarterly|yearly|allTime`), optionally broken down with `--dimension-id` and filtered with `--filters`. It works for every kind of metric, including integration metrics (pass `--source-id` for those). The platform may sync the metric first, so a first call can take about two minutes; a `Sync:` line on stderr, or `syncInfo` (`{status, lastSyncedAt, message}`) in `--json`, says how current the values are. See [Reading Metric Values](https://github.com/databox/databox-cli#reading-metric-values).
- **New: `dashboard` commands.** `dashboard list`, `dashboard metrics DASHBOARDID` and `dashboard rename DASHBOARDID --name`, which renames a dashboard (an Admin or Editor any dashboard they can see, a User one they created). See [Dashboards](https://github.com/databox/databox-cli#dashboards).
- **Deprecated: `databoard list` and `databoard metrics`.** They keep working, with the same output as `dashboard list` and `dashboard metrics`, and print a deprecation warning on stderr. They will be removed in a future major version; switch scripts to `dashboard`.
- **Metric descriptions.** `metric get` shows a metric's description, and `metric list --json` and `metric get --json` carry it as `description` (`null` when there is none). `metric update --description` sets it on a dataset-based custom metric, and `--description ""` clears it. See [Metric Descriptions](https://github.com/databox/databox-cli#metric-descriptions).
- **Merged datasets.** `dataset list --type mergedDataset` lists merged datasets (`--type dataset`, the default, lists the others). Datasets now report their `type` in `--json` and in a Type column of `dataset list`; a merged dataset has no data source.
- **Errors show when to retry.** When the API sends `Retry-After`, the error adds a `Retry after N s` line. That covers `429 rate_limited` and `409 modification_in_progress`, which dataset reads and metric commands return for a few seconds after a dataset modification is saved. The CLI does not retry by itself. See [Errors and Exit Codes](https://github.com/databox/databox-cli#errors-and-exit-codes).
- **`metric get` lists no `hourly` granularity** any more, as the API no longer offers it for metrics; `metric data` does not accept it.
- **A request ID on every request, and in every error.** The CLI sends its own `x-request-id` with each request. Every error now shows a Request ID, including a bare `413` or `502` from a proxy, a network failure and a timeout; where no response came back, it is the CLI's own ID, and still the one to quote to support. A timed-out `dataset ingest` may still have been processed: check `dataset ingestions`. See [Errors and Exit Codes](https://github.com/databox/databox-cli#errors-and-exit-codes).
- **`--api-url`, `DATABOX_API_URL` and a stored API URL must be an `http://` or `https://` URL**, with no username, password, query string or fragment; surrounding whitespace is ignored. A bad value exits 2 (1 when it is the stored one) before anything is sent.
- **User-Agent.** Requests identify themselves as `databox-cli/<version> (node <version>; <platform>)`.
- **Ingest payload limit corrected to 10 MB** (10,485,760 bytes), what the API accepts. 1.0.0 allowed 30 MB, so a payload between the two was sent and then refused by the API with `413`; the CLI now refuses it before sending (exit 2).
- **Packaging fixes** in `package.json` (the `bin` path and repository URL npm warned about at publish), and dependency updates in the lockfile.
Expand Down
451 changes: 330 additions & 121 deletions README.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "databox-cli",
"version": "1.0.1",
"version": "1.1.0",
"type": "module",
"description": "CLI for the Databox public API",
"author": "Databox",
Expand Down Expand Up @@ -46,8 +46,11 @@
"data-source": {
"description": "Manage data sources"
},
"dashboard": {
"description": "Browse dashboards and their metrics, and rename them"
},
"databoard": {
"description": "Browse databoards and their metrics"
"description": "Deprecated: use dashboard"
},
"dataset": {
"description": "Manage datasets and ingest data"
Expand Down
4 changes: 4 additions & 0 deletions skills/databox-datasets/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ A refusal is `forbidden` (exit 1) and nothing is changed. Do not retry it: tell
|------|---------|
| List datasets | `databox dataset list` |
| List a data source's datasets | `databox dataset list --data-source-id ID` |
| List merged datasets | `databox dataset list --type mergedDataset` |
| Create dataset | `databox dataset create --name "Name" --data-source-id ID` |
| Get dataset details | `databox dataset get ID` |
| View schema and primary key | `databox dataset schema ID` |
Expand Down Expand Up @@ -154,6 +155,8 @@ databox dataset preview-modification 67890 --data "$(cat mod.json)" # up to 20
databox dataset update-modification 67890 --data "$(cat mod.json)"
```

A saved modification is applied in the background, usually within a few seconds. Until it is, `dataset data`, `dataset schema`, `dataset metadata`, `dataset column-metadata`, `dataset set-metadata --default-time-dimension`, `dataset set-column-metadata` and `dataset clear-modifications` answer `409 modification_in_progress` with a `Retry after N s` line: wait that long and run the same command again. `dataset get`, `dataset list` and `dataset modifications` are never refused. If the modification could not be applied, the reads and those `set-` commands answer `409 modification_failed`, and retrying will not help: save it again with `update-modification`, or remove it with `clear-modifications`, which a failed modification does not block.

`dataset modification-rules` lists the filter operators (`type`) and type conversions (`outputLogicalType`) each column type accepts; `dataset modification-functions` lists the functions formulas can use.

## Permissions
Expand All @@ -171,6 +174,7 @@ These commands prompt for confirmation at a terminal. Off a terminal (scripts, a

- All commands support `--json` and `--output csv` for machine-readable output
- `dataset schema`, `dataset data` and `dataset preview-modification` return the whole response under `--json` (rows or columns under `items`, plus `schema`/`primaryKey`)
- Every dataset reports its `type`: `dataset` or `mergedDataset`. `dataset list` shows plain datasets unless given `--type mergedDataset`; a merged dataset has no data source, so the API refuses `--type mergedDataset` with `--data-source-id`
- Dataset IDs are numeric (e.g., `67890`), separate from the data source ID. Find one with `databox dataset list --search NAME` or `databox data-source datasets DATASOURCEID`; in the Databox app it is the number in the Data Manager page URL (`/data-manager/connected/67890/datasets/view`). 0.x GUIDs are rejected.
- Ingestion IDs are UUIDs returned by the `ingest` command
- `--interval` on `set-sync-frequency` is in minutes: 1, 15, 60, 240, 360, 480 or 1440; `sync-frequency-options` shows which your plan includes
49 changes: 43 additions & 6 deletions skills/databox-metrics/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: databox-metrics
description: Use when the user wants to create, manage, or query metrics in Databox — including custom metrics, dimensions and dimension values, drilldown into the rows behind a metric, metric lineage and usages, or verification. Triggers on mentions of metrics, KPIs, custom queries, metric builder, drilldown, or where a metric is used in Databox.
description: Use when the user wants to create, manage, or query metrics in Databox — including a metric's values over a date range, custom metrics and their descriptions, dimensions and dimension values, drilldown into the rows behind a metric, metric lineage and usages, verification, or the dashboards a metric is on. Triggers on mentions of metrics, KPIs, custom queries, metric builder, drilldown, dashboards, or where a metric is used in Databox.
---

# Databox Metrics Management
Expand All @@ -21,15 +21,18 @@ Must be authenticated. If not, use the `databox-auth` skill first.
| Get metric detail | `databox metric get METRIC_ID` |
| Create metric | `databox metric create --name "Revenue" --dataset-id 123 --measure '{"id":"amount","displayName":"Amount"}' --date '{"id":"date","displayName":"Date"}'` |
| Update metric | `databox metric update METRIC_ID --name "New Name"` |
| Set or clear the description | `databox metric update METRIC_ID --description "Net of refunds"` / `--description ""` |
| Delete metric | `databox metric delete METRIC_ID --force` |
| Values over a date range | `databox metric data --metric-id METRIC_ID --from 2026-06-01 --to 2026-06-30 --granularity daily` |
| Rows behind a value | `databox metric drilldown --metric-id METRIC_ID --start-timestamp 1704067200 --end-timestamp 1735689600` |
| Dimension values | `databox metric dimension-values --metric-id METRIC_ID --source-id 123 --dimension-id country` |
| Lineage | `databox metric lineage METRIC_ID` |
| Where it is used | `databox metric usages METRIC_ID` |
| View verification | `databox metric verification METRIC_ID` |
| Set verification | `databox metric set-verification METRIC_ID --status verified` |
| List databoards | `databox databoard list --search marketing` |
| Metrics on a databoard | `databox databoard metrics DATABOARD_ID` |
| List dashboards | `databox dashboard list --search marketing` |
| Metrics on a dashboard | `databox dashboard metrics DASHBOARD_ID` |
| Rename a dashboard | `databox dashboard rename DASHBOARD_ID --name "Q4 pipeline"` |

## Metric IDs

Expand Down Expand Up @@ -58,7 +61,39 @@ databox metric create --name "Revenue by country" --dataset-id 123 \
- `--filters` is one group: `{"logicalOperator": "and"|"or", "conditions": [{"field", "operator", "values"}]}`
- `create` prints the new metric, including its ID; add `--idempotency-key "$(uuidgen)"` if a retry must not create a second one

On `metric update`, omitted flags keep their current values. `--dimension` replaces the whole dimension list and `--clear-dimensions` removes it. In `--filters`, leaving out `conditions` keeps the stored ones (so `{"logicalOperator":"or"}` changes only the operator), and `"conditions": []` clears them.
On `metric update`, omitted flags keep their current values. `--description` sets the description and `--description ""` clears it; `metric create` takes none, so set it with `metric update` afterwards. A cleared description reads back as `null`. The update's output shows the change at once; `metric get` and `metric list` can lag a few seconds. `--dimension` replaces the whole dimension list and `--clear-dimensions` removes it. In `--filters`, leaving out `conditions` keeps the stored ones (so `{"logicalOperator":"or"}` changes only the operator), and `"conditions": []` clears them.

## Metric Values

`metric data` returns a metric's values over a date range, one per `--granularity` bucket. It works for every kind of metric: integration, custom, calculated and pushed.

| Flag | Required | Description |
|------|----------|-------------|
| `--metric-id` | Yes | Metric ID |
| `--source-id` | See description | Defaults to the part of `--metric-id` before `\|`; if given, it must match. Required for an ID without one, e.g. `GoogleAnalytics4@sessions` (use `sourceId` from `metric list`) |
| `--from`, `--to` | Yes | `YYYY-MM-DD`; `--from` must not be after `--to` |
| `--granularity` | Yes | `daily`, `weekly`, `monthly`, `quarterly`, `yearly` or `allTime` (one bucket for the whole range). No `hourly` |
| `--dimension-id` | No | Dimension to break down by; repeat for several |
| `--filters` | No | One flat group: `{"logicalOperator":"AND","conditions":[{"type":"dimension","field":"country","operator":"ANY_OF","values":["US"]}]}`. Operators: `ANY_OF`, `NONE_OF`, `EQ`, `NEQ`, `CONTAINS`, `NOT_CONTAINS`. Operators are case-insensitive (`"and"`, as `metric get` shows it, works). `field` is required. Not drilldown's nested `groups` |

Bucket dates are in the source's timezone. The table and CSV are followed by a `Sync: <status> — <message>` line on stderr; `--json` returns `{metric, dataPoints, syncInfo}`, with `syncInfo` as `{status, lastSyncedAt, message}`. Read `syncInfo` before trusting the values:

| `syncInfo.status` | What to do |
|------|------|
| `synced` | This call synced the metric: use the values. A `null` value means the bucket has no data |
| `skipped` | No sync was needed: use the values, current as of `lastSyncedAt`. Calling again sooner does not sync |
| `inProgress` | A sync is running; values may be missing or stale. Call again in about a minute |
| `neverSynced` | Not synced yet; values may be empty. Call again shortly |
| `failed` | The last sync failed; values are what was stored before. Retrying won't help until the data source (e.g. its connection) is fixed |
| any other | Values may not be current |

`syncInfo` is `null` (the stderr line reads `Sync: unknown`) for a metric that never syncs, such as a constant.

The first call on a metric that has never synced can take about two minutes. Errors: `504 service_unavailable` means the sync outlived the API's 150 s and is still running, so retry shortly. The endpoint allows 30 calls per account per minute; `429 rate_limited` prints `Retry after N s`, so wait that long. Do not loop on it.

## Dataset Modifications Being Applied

For a few seconds after a dataset modification is saved, `metric create`, `metric update`, and `metric data`, `metric drilldown` and `metric dimension-values` on a dataset metric answer `409 modification_in_progress` with a `Retry after N s` line: wait that long and run the same command again. `409 modification_failed` is final: the modification has to be saved again (`dataset update-modification`) or removed (`dataset clear-modifications`).

## Drilldown

Expand All @@ -74,7 +109,7 @@ On `metric update`, omitted flags keep their current values. `--dimension` repla
| `--sort-by`, `--sort-order` | No | Sort by a column id; `--sort-order` needs `--sort-by` |
| `--page`, `--page-size`, `--all` | No | `--page-size` up to 1000 |

To reproduce what a databoard shows, take the metric's dimensions and applied filters from `databox databoard metrics DATABOARD_ID --json` and pass them as `--dimension-id` and `--filters`. Without them you get every row in the period. `--json` returns the whole response: rows under `items`, plus `schema` and `pagination`.
To reproduce what a dashboard shows, take the metric's dimensions and applied filters from `databox dashboard metrics DASHBOARD_ID --json` and pass them as `--dimension-id` and `--filters`. Without them you get every row in the period. `--json` returns the whole response: rows under `items`, plus `schema` and `pagination`.

## Dimension Values

Expand All @@ -89,4 +124,6 @@ To reproduce what a databoard shows, take the metric's dimensions and applied fi

- All commands support `--json` and `--output csv` for machine-readable output
- Use `--source-id` to filter the metric list by data source or dataset
- There is no command that loads a metric's aggregated values over time; use `metric drilldown` for the underlying rows
- `metric data` loads a metric's values over time; `metric drilldown` returns the rows behind a dataset metric's value, without a sync
- `databoard list` and `databoard metrics` are deprecated names for `dashboard list` and `dashboard metrics`; use `dashboard`
- `dashboard rename`: an Admin or Editor can rename any dashboard they can see, a User only one they created. The API refuses a blank name, one over 65 characters, or one with a tab or line break inside
28 changes: 11 additions & 17 deletions src/base-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import {
} from '@oclif/core'

import {
ApiClient, ApiConnectionError, ApiRequestError, describeApiError, describeConnectionError, describeInvalidApiUrl, isApiUrl,
ApiClient, ApiConnectionError, ApiRequestError, describeApiError, describeConnectionError, describeInvalidApiUrl, normalizeApiUrl,
} from './lib/api-client.js'
import {getConfigPath, loadConfig} from './lib/config.js'
import {loadConfig, storedApiUrl} from './lib/config.js'
import {OUTPUT_FORMATS, OutputFormat, colorEnabled} from './lib/output.js'

export type Flags<T extends typeof Command> = Interfaces.InferredFlags<T['flags'] & typeof BaseCommand['baseFlags']>
Expand Down Expand Up @@ -100,10 +100,12 @@ export abstract class BaseCommand<T extends typeof Command = typeof Command> ext
const accountId = this.flags['account-id']
if (accountId) this.requireNumericId(accountId, '--account-id')

// A base URL fetch cannot use would otherwise fail only once the request is built: see isApiUrl().
const apiUrl = this.flags['api-url']
if (apiUrl !== undefined && !isApiUrl(apiUrl)) {
this.error(describeInvalidApiUrl(apiUrl), {exit: 2})
// A base URL fetch cannot use would otherwise fail only once the request is built: see normalizeApiUrl().
const apiUrlFlag = this.flags['api-url']
let apiUrl: string | undefined
if (apiUrlFlag !== undefined) {
apiUrl = normalizeApiUrl(apiUrlFlag)
if (apiUrl === undefined) this.error(describeInvalidApiUrl(apiUrlFlag), {exit: 2})
}

// Every command that extends BaseCommand calls the API, so a missing key or an unreadable
Expand All @@ -115,19 +117,11 @@ export abstract class BaseCommand<T extends typeof Command = typeof Command> ext
this.error('Not authenticated. Run "databox auth login" first.', {exit: 1})
}

// A URL saved before `auth login` checked it, or edited in by hand. Like an unreadable
// config, it is the stored config at fault, so exit 1; the --api-url above overrides it.
if (apiUrl === undefined && config.apiUrl !== undefined && !isApiUrl(config.apiUrl)) {
this.error(
`The API URL saved in ${getConfigPath()} ("${config.apiUrl}") is not an http:// or https:// URL. `
+ 'Run "databox auth login --api-url https://api.databox.com" to replace it.',
{exit: 1},
)
}

// A URL saved before `auth login` checked it, or edited in by hand, fails with exit 1;
// the --api-url above overrides it, so it is not checked then.
this.apiClient = new ApiClient({
apiKey,
baseUrl: apiUrl ?? config.apiUrl,
baseUrl: apiUrl ?? storedApiUrl(config),
trace: this.flags.verbose ? line => this.logToStderr(line) : undefined,
version: this.config.version,
})
Expand Down
Loading
Loading