diff --git a/plugins/trogonstack-datadog/skills/datadog-design-dashboard/SKILL.md b/plugins/trogonstack-datadog/skills/datadog-design-dashboard/SKILL.md index f02bb8e..b1af480 100644 --- a/plugins/trogonstack-datadog/skills/datadog-design-dashboard/SKILL.md +++ b/plugins/trogonstack-datadog/skills/datadog-design-dashboard/SKILL.md @@ -3,8 +3,8 @@ name: datadog-design-dashboard description: >- Design new Datadog dashboards, redesign existing ones, or audit dashboards for operational readiness. Covers widget selection, layout organization, - template variables, group structure, alert threshold validation, and - zero-knowledge readability. Uses pup CLI for inspecting dashboards and + template variables, group structure, tab organization, alert threshold + validation, and zero-knowledge readability. Uses pup CLI for inspecting dashboards and validating designs. Use when designing new dashboards, redesigning existing ones, auditing before on-call handoff, or reviewing after dashboard changes. Do not use for: (1) Datadog agent installation or configuration, @@ -124,7 +124,13 @@ Using your domain understanding and the chosen framework, design the group struc - **[widgets.md](references/widgets.md)** — Widget selection guide, display options, sizing, naming conventions - **[thresholds.md](references/thresholds.md)** — Alert threshold markers, threshold proximity, Y-axis configuration -### 5. Write the design output +### 5. Consider tabs + +If the dashboard has 7+ top-level groups, evaluate whether tabs would reduce scroll fatigue. Read [references/tabs.md](references/tabs.md) for the organization pattern, JSON schema, and common mistakes. + +Before adding tabs to an existing dashboard, audit all widget IDs for duplicates — tabs trigger strict uniqueness validation. + +### 6. Write the design output Present the design using this template: @@ -261,7 +267,15 @@ Evaluate: | Runbook/ownership note | MISSING | No note widget with team info | ``` -### 7. Generate audit report +### 7. Audit tab organization + +If the dashboard has 7+ top-level groups, check whether tabs are in use. See [references/tabs.md](references/tabs.md). + +- Are related groups consolidated into tabs by observability layer? +- Do tab names reflect the question they answer (Service Health, Platform, Infrastructure)? +- Are all top-level widgets assigned to exactly one tab? + +### 8. Generate audit report Compile all findings into a structured report: @@ -333,3 +347,4 @@ Compile all findings into a structured report: - **[Layout & Structure](references/layouts.md)** — Template variables, group patterns, dashboard strategy, grid sizing, anti-patterns - **[Widgets](references/widgets.md)** — Widget prefix system, types, display options, sizing, naming conventions - **[Alert Thresholds](references/thresholds.md)** — Threshold markers, proximity guide, Y-axis configuration +- **[Tabs](references/tabs.md)** — Dashboard tab organization, JSON schema, `@N` positional references, common mistakes diff --git a/plugins/trogonstack-datadog/skills/datadog-design-dashboard/references/layouts.md b/plugins/trogonstack-datadog/skills/datadog-design-dashboard/references/layouts.md index 6ad6539..1593fd8 100644 --- a/plugins/trogonstack-datadog/skills/datadog-design-dashboard/references/layouts.md +++ b/plugins/trogonstack-datadog/skills/datadog-design-dashboard/references/layouts.md @@ -340,3 +340,4 @@ Datadog uses a 12-column grid. | Domain-specific filters in platform groups | Platform group silently shows only one domain; new domains are invisible | Platform groups (Commanded, Oban, Broadway, etc.) must scope only by template variables (`$env`, `$service`). Hardcoded handler names, queue names, or domain names belong in domain groups, not platform groups. | | Individual handler/worker widget when global `by {dimension}` view exists | Redundant widget adds noise without adding signal | Before adding a widget scoped to a specific handler, queue, or worker in a platform group, check: does a global `by {handler_name}` / `by {queue}` timeseries already exist? If yes, the specific widget adds nothing — the global view already surfaces it when it spikes. Only add specific widgets when they have their own alert threshold or SLO that justifies the dedicated callout, and place them in the domain group, not the platform group. | | Transport metrics (`P`) placed in Business group | Misleads readers into thinking protocol health = business health; obscures what domain outcomes actually are | gRPC error rate, HTTP request rate, and apdex are `P` regardless of placement. See [widgets.md](widgets.md) for the full B trap guide. | +| 13+ groups without tabs | Endless scrolling, no way to jump to the right layer during an incident | Organize groups into tabs by observability layer (Service Health, Platform, Infrastructure). See [tabs.md](tabs.md). | diff --git a/plugins/trogonstack-datadog/skills/datadog-design-dashboard/references/tabs.md b/plugins/trogonstack-datadog/skills/datadog-design-dashboard/references/tabs.md new file mode 100644 index 0000000..bbda3fc --- /dev/null +++ b/plugins/trogonstack-datadog/skills/datadog-design-dashboard/references/tabs.md @@ -0,0 +1,146 @@ +# Tabs + +Dashboard tabs organize top-level widget groups into named sections. Tabs reduce scroll fatigue on large dashboards and let viewers navigate directly to the layer they care about during an incident. + +--- + +## When to Use Tabs + +| Groups | Recommendation | +|--------|----------------| +| 1-6 | Tabs add friction without saving scroll. Skip. | +| 7-12 | Consider tabs if groups span distinct observability layers. | +| 13+ | Tabs strongly recommended. A 15-group dashboard is unusable without them. | + +--- + +## Tab Organization Pattern + +Group tabs by observability layer, not by arbitrary concern. Each tab answers a different question during an incident. + +| Tab | Question | Typical groups | +|-----|----------|---------------| +| **Business** or **Service Health** | Are customers affected? | Service health, domain groups (by bounded context), domain-specific drill-downs | +| **Platform** | Is the application pipeline healthy? | CQRS/ES, message queues, job processors, database clients, logging | +| **Infrastructure** | Are the underlying systems healthy? | HTTP servers, pods, RDS, BEAM, DNS, load balancers | +| **Testing** | How did the last load test go? | k6, Locust, or other load test groups | + +Adapt the tab names and groupings to the service. A service with no CQRS layer does not need a Platform tab. A service with no load tests does not need a Testing tab. + +--- + +## JSON Schema + +Tabs are a top-level dashboard field, sibling to `widgets`. Widgets stay in a flat array; tabs reference them by position. + +```json +{ + "tabs": [ + { + "name": "Business", + "widget_ids": ["@1", "@2", "@5"] + }, + { + "name": "Platform", + "widget_ids": ["@3", "@4", "@8"] + }, + { + "name": "Infrastructure", + "widget_ids": ["@6", "@7"] + } + ], + "widgets": [ ... ] +} +``` + +### Field reference + +| Field | Type | Required | Notes | +|-------|------|----------|-------| +| `name` | string | yes | Display name of the tab. NOT `tab_name`. | +| `id` | string (UUID) | no | Auto-generated by the Terraform provider using `uuid5(Nil, "tab-{index}")`. Include explicitly for stability across applies. | +| `widget_ids` | array of strings | yes | References to top-level widgets. NOT `tab_id` for the field name. | + +### Positional references (`@N`) + +Widget IDs use `@N` notation — **1-indexed strings** referencing position in the `widgets` array. + +- `"@1"` = first widget in the array +- `"@15"` = fifteenth widget in the array +- Must be strings (`"@1"`), not integers +- The Datadog API resolves `@N` to actual widget IDs server-side +- The Terraform provider converts API-returned integer IDs back to `@N` on read + +**Prefer `@N` over hardcoded integer IDs.** Integer widget IDs are unstable — the API assigns them on creation and they change if widgets are recreated. `@N` references are positional and survive widget ID changes. + +--- + +## Common Mistakes + +### Wrong field names + +The Datadog API documentation uses `tab_name` and `tab_id`. The actual API and Terraform provider expect `name` and `id`. Using the documented names returns a 400 error. + +```json +// WRONG — returns 400 +{"tab_name": "Overview", "tab_id": "...", "widget_ids": [...]} + +// CORRECT +{"name": "Overview", "id": "...", "widget_ids": [...]} +``` + +### Duplicate widget IDs + +Adding tabs triggers strict widget ID uniqueness validation that the API previously ignored. If your dashboard JSON has widgets with duplicate `id` fields (common when widgets were copy-pasted), the API will reject the update with `"Duplicate widget IDs found"`. + +**Before adding tabs**: audit all widget IDs for duplicates. + +```python +import json +from collections import Counter + +with open("dashboard.json") as f: + d = json.load(f) + +all_ids = [] +def collect_ids(widgets): + for w in widgets: + wid = w.get("id") + if wid is not None: + all_ids.append(wid) + for nested in w.get("definition", {}).get("widgets", []): + collect_ids([nested]) + +collect_ids(d["widgets"]) +dupes = {k: v for k, v in Counter(all_ids).items() if v > 1} +print(f"Duplicates: {dupes}" if dupes else "No duplicates") +``` + +### Every widget must be assigned + +All top-level widgets must appear in exactly one tab. Missing a widget does not hide it — the API may reject the request or behave unpredictably. + +### Provider version + +Tab support requires Terraform provider `datadog/datadog >= 3.44.0`. Earlier versions silently pass the `tabs` field through but cannot normalize `@N` references on read, causing perpetual diffs. + +--- + +## Terraform Provider Internals + +Understanding the provider lifecycle prevents debugging dead ends. + +### On create/update (write path) + +1. `stripDeprecatedFields()` removes `is_read_only` from the JSON +2. Same function injects deterministic UUIDs for any tab missing an `id`: `uuid5(Nil, "tab-{index}")` +3. The JSON — including `@N` references — is sent as-is to `PUT /api/v1/dashboard/{id}` +4. The API resolves `@N` to actual integer widget IDs server-side + +### On read (state path) + +1. `prepResource()` strips computed fields (`id`, `author_handle`, `author_name`, `created_at`, `modified_at`, `url`) for diff comparison +2. Same function builds a `widgetID → position` map and converts integer `widget_ids` back to `@N` strings +3. Tab `id` fields are stripped from state to prevent diff noise + +This means: the provider handles the `@N` ↔ integer conversion transparently. You write `@N`, the API receives `@N`, the API returns integers, the provider converts back to `@N` for state.