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
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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:

Expand Down Expand Up @@ -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:

Expand Down Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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). |
Original file line number Diff line number Diff line change
@@ -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.

Comment thread
yordis marked this conversation as resolved.
---

## 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.