Measure PR delivery metrics
Understanding how pull requests flow through your engineering organization is essential for identifying delivery bottlenecks, measuring team efficiency, and driving continuous improvement. Without PR-level visibility, engineering leaders lack the data needed to spot stale reviews, unbalanced workloads, or degrading cycle times before they impact delivery.
This guide walks you through building a comprehensive PR delivery metrics dashboard in Port that answers critical questions at both the service level and team level. While this guide demonstrates implementations using GitHub, GitLab, and Azure DevOps, other Git providers can be used as well.
- Throughput: How many PRs are being merged weekly and monthly?
- Cycle time: How long does it take from PR creation to merge?
- Staleness: Which PRs have been open for more than 7 days, and what share do they represent?
- Quality indicators: Do open PRs have reviewers and assignees assigned?
By the end of this guide, you will have a dashboard that provides full visibility into PR delivery health across services and teams, helping you identify improvement areas and track progress over time.
Common use cases
- Track PR cycle time trends to identify slowdowns in code review and CI processes.
- Monitor PR throughput to understand team delivery capacity and detect regressions.
- Surface stale PRs that have been open longer than 7 days to unblock delivery.
- Identify PRs without assigned reviewers or assignees to improve process compliance.
- Compare delivery metrics across teams to understand organizational performance.
Prerequisites
This guide assumes the following:
- You have a Port account and completed the onboarding process.
- You have access to a repository (GitHub, GitLab, or Azure Repos) that is connected to Port via the onboarding process.
- The foundational data model is in place (i.e.
serviceandTeamblueprints exist with ateamrelation from service to team). - One of the following integrations is installed:
- GitHub: Port's GitHub integration or GitHub Ocean integration that creates
githubPullRequest,githubRepository,githubUser, andgithubTeamblueprints. - GitLab: Port's GitLab integration that creates
gitlabMergeRequest,gitlabRepository,gitlabGroup, andgitlabMemberblueprints. - Azure DevOps: Port's Azure DevOps integration that creates
azureDevopsPullRequest,azureDevopsRepository,azureDevopsProject, andazureDevopsUserblueprints.
- GitHub: Port's GitHub integration or GitHub Ocean integration that creates
Key metrics overview
This dashboard tracks PR delivery metrics across two levels — individual services and teams:
| Metric | What it measures | Why it matters |
|---|---|---|
| Open PRs | Total number of currently open PRs | Shows current work in progress and potential bottleneck signals |
| PR throughput | Number of PRs merged per week/month | Indicates delivery flow and team output capacity |
| Throughput trend | Weekly vs. monthly throughput direction | Reveals whether delivery velocity is improving or degrading |
| Stale PRs | PRs open longer than 7 days | Highlights blocked work, unclear ownership, or review delays |
| Stale PR share (%) | Percentage of open PRs that are stale | Quantifies the severity of staleness across the portfolio |
| Cycle time | Hours from PR creation to merge (weekly/monthly avg) | Exposes friction in reviews, CI, and approval processes |
| Cycle time trend | Weekly vs. monthly cycle time direction | Shows whether review and merge speed is improving over time |
| Reviewer coverage | Whether open PRs have reviewers assigned | Signals process compliance and review readiness |
| Assignee coverage | Whether open PRs have assignees assigned | Indicates ownership clarity for open work |
Set up data model
Your Git integration automatically creates pull request / merge request blueprints with default properties. We need to extend these with additional properties for cycle time, staleness tracking, and quality indicators, then add aggregation and calculation properties to the service and Team blueprints to surface metrics at those levels.
GitHub and Azure DevOps use "pull requests" while GitLab uses "merge requests". The concepts are equivalent — this guide uses "PR" generically to refer to both.
Update the pull request / merge request blueprint
Add properties for cycle time measurement, quality indicators, and staleness tracking to your existing PR/MR blueprint.
-
Go to the Builder page of your portal.
-
Find your PR/MR blueprint (GitHub Pull Request, Merge Request, or Pull Request for Azure DevOps) and click on it.
-
Click on the
...button in the top right corner, and choose{...} Edit JSON. -
Add the following properties to the
propertiessection of theschemaobject (alongside the existing default properties):- GitHub
- GitLab
- Azure DevOps
Additional PR properties (click to expand)
"cycle_time_hours": {
"title": "PR Cycle Time (Hours)",
"type": "number",
"description": "Time from PR creation to merge in hours"
},
"has_assignees": {
"title": "Has Assignees",
"type": "boolean",
"description": "Whether the PR has at least one assignee"
},
"has_reviewers": {
"title": "Has Reviewers",
"type": "boolean",
"description": "Whether the PR has at least one reviewer assigned"
},
"reviewDecision": {
"title": "Review Decision",
"type": "string"
}Additional MR properties (click to expand)
"cycle_time_hours": {
"title": "MR Cycle Time (Hours)",
"type": "number",
"description": "Time from MR creation to merge in hours"
},
"leadTimeHours": {
"title": "Lead Time (hours)",
"type": "number",
"description": "Time from MR creation to merge in hours"
},
"has_assignees": {
"title": "Has Assignees",
"type": "boolean",
"description": "Whether the MR has at least one assignee"
},
"has_reviewers": {
"title": "Has Reviewers",
"type": "boolean",
"description": "Whether the MR has at least one reviewer assigned"
}GitLab status valuesGitLab merge requests use
opened(notopen),merged,closed, andlockedas status values. The default blueprint already includesstatus,createdAt,updatedAt,mergedAt, andlinkproperties.Additional PR properties (click to expand)
"cycle_time_hours": {
"title": "PR Cycle Time (Hours)",
"type": "number",
"description": "Time from PR creation to completion in hours"
},
"has_reviewers": {
"title": "Has Reviewers",
"type": "boolean",
"description": "Whether the PR has at least one reviewer assigned"
}Azure DevOps status valuesAzure DevOps pull requests use
active(notopen),completed(equivalent to merged), andabandonedas status values. The default blueprint already includesstatus,createdAt,link, anddescriptionproperties. Note that Azure DevOps does not have amergedAtproperty so we will useclosedDatefor the calculations. -
Add the following entry to the existing
mirrorPropertiessection. This mirror traverses theservice → teamrelation chain to surface the owning team's name on each PR, which the "Teams with Highest PR Throughput" bar chart uses as its breakdown property:PR mirror property (click to expand)
"team_name": {
"title": "Team",
"path": "service.team.$title"
} -
Add the following entries to the existing
calculationPropertiessection:- GitHub
- GitLab
- Azure DevOps
PR calculation properties (click to expand)
"days_old": {
"title": "Days Old",
"type": "number",
"calculation": "(now / 86400) - (.properties.createdAt | capture(\"(?<date>\\\\d{4}-\\\\d{2}-\\\\d{2})\") | .date | strptime(\"%Y-%m-%d\") | mktime / 86400) | floor"
},
"is_stale": {
"title": "Is Stale (7d+)",
"type": "boolean",
"calculation": "if .properties.status == \"open\" then ((now - (.properties.createdAt | sub(\"\\\\.[0-9]+Z$\"; \"Z\") | fromdateiso8601)) / 86400) > 7 else false end"
},
"pr_age_label": {
"title": "PR Age Label",
"type": "string",
"calculation": "((now - (.properties.createdAt | sub(\"\\\\.[0-9]+Z$\"; \"Z\") | fromdateiso8601)) / 86400 | round) as $age | if $age <= 3 then \"0-3 days\" elif $age <= 7 then \"3-7 days\" elif $age <= 30 then \"7-30 days\" else \">30 days\" end",
"colorized": true,
"colors": {
"0-3 days": "green",
"3-7 days": "orange",
"7-30 days": "red",
">30 days": "red"
}
}Existing relationsThe default
githubPullRequestblueprint already includes relations forrepository,git_hub_creator,git_hub_assignees,git_hub_reviewers, and a dynamicservicerelation. No changes to relations are needed.MR calculation properties (click to expand)
"days_old": {
"title": "Days Old",
"type": "number",
"calculation": "(now / 86400) - (.properties.createdAt | capture(\"(?<date>\\\\d{4}-\\\\d{2}-\\\\d{2})\") | .date | strptime(\"%Y-%m-%d\") | mktime / 86400) | floor"
},
"is_stale": {
"title": "Is Stale (7d+)",
"type": "boolean",
"calculation": "if (.properties.status == \"opened\" and (((now / 86400) - (.properties.createdAt | capture(\"(?<date>\\\\d{4}-\\\\d{2}-\\\\d{2})\") | .date | strptime(\"%Y-%m-%d\") | mktime / 86400) | floor) > 7)) then true else false end",
"colorized": true,
"colors": {
"true": "orange"
}
},
"pr_age_label": {
"title": "MR Age",
"type": "string",
"calculation": "((if .properties.mergedAt == null then (now / 86400) else (.properties.mergedAt | capture(\"(?<date>\\\\d{4}-\\\\d{2}-\\\\d{2})\") | .date | strptime(\"%Y-%m-%d\") | mktime / 86400) end) as $toDate | $toDate - (.properties.createdAt | capture(\"(?<date>\\\\d{4}-\\\\d{2}-\\\\d{2})\") | .date | strptime(\"%Y-%m-%d\") | mktime / 86400) | floor) as $daysOld | if $daysOld <= 3 then \"0-3 days\" elif $daysOld <= 7 then \"3-7 days\" elif $daysOld <= 30 then \"7-30 days\" else \">30 days\" end",
"colorized": true,
"colors": {
"0-3 days": "green",
"3-7 days": "yellow",
"7-30 days": "orange",
">30 days": "red"
}
}Existing relationsThe default
gitlabMergeRequestblueprint already includes relations forrepository,service, and creator/reviewer/assignee relations. Ensure yourservicerelation maps correctly to your service blueprint.PR calculation properties (click to expand)
"days_old": {
"title": "Days Old",
"type": "number",
"calculation": "(now / 86400) - (.properties.createdAt | capture(\"(?<date>\\\\d{4}-\\\\d{2}-\\\\d{2})\") | .date | strptime(\"%Y-%m-%d\") | mktime / 86400) | floor"
},
"is_stale": {
"title": "Is Stale (7d+)",
"type": "boolean",
"calculation": "if (.properties.status == \"active\" and (((now / 86400) - (.properties.createdAt | capture(\"(?<date>\\\\d{4}-\\\\d{2}-\\\\d{2})\") | .date | strptime(\"%Y-%m-%d\") | mktime / 86400) | floor) > 7)) then true else false end",
"colorized": true,
"colors": {
"true": "orange"
}
},
"pr_age_label": {
"title": "PR Age",
"type": "string",
"calculation": "((if .properties.status != \"active\" and .properties.createdAt != null then ((now / 86400) | floor) else (now / 86400) end) as $toDate | $toDate - (.properties.createdAt | capture(\"(?<date>\\\\d{4}-\\\\d{2}-\\\\d{2})\") | .date | strptime(\"%Y-%m-%d\") | mktime / 86400) | floor) as $daysOld | if $daysOld <= 3 then \"0-3 days\" elif $daysOld <= 7 then \"3-7 days\" elif $daysOld <= 30 then \"7-30 days\" else \">30 days\" end",
"colorized": true,
"colors": {
"0-3 days": "green",
"3-7 days": "yellow",
"7-30 days": "orange",
">30 days": "red"
}
}Existing relationsThe default
azureDevopsPullRequestblueprint already includes relations forrepository,azure_devops_reviewers,azure_devops_creator, andservice. Ensure yourservicerelation maps correctly to your service blueprint. -
Click Save to update the blueprint.
Update the service blueprint
Add aggregation and calculation properties to the service blueprint so that each service displays its own PR delivery metrics.
-
Go to your Builder page.
-
Find the Service blueprint and click on it.
-
Click on the
...button in the top right corner, and choose{...} Edit JSON. . -
Add the following entry to the
relationssection of the blueprint. This relation links each service to the team that owns it, enabling team-level aggregation of PR metrics through thePR → service → teampath:Service team relation (click to expand)
"team": {
"title": "Team",
"target": "_team",
"required": false,
"many": false
} -
Add the following entry to the
mirrorPropertiessection of the blueprint. This mirror surfaces the parent team name on each service, allowing the dashboard to group services by their parent team in the "Service Performance Overview" table:Service mirror property (click to expand)
"parent_team_name": {
"title": "Parent Team",
"path": "team.parent_team.$title"
} -
Add the following entries to the
aggregationPropertiessection of the blueprint:- GitHub
- GitLab
- Azure DevOps
Service aggregation properties (click to expand)
"open_prs": {
"title": "Open PRs",
"type": "number",
"target": "githubPullRequest",
"query": {
"combinator": "and",
"rules": [
{ "property": "status", "operator": "=", "value": "open" }
]
},
"calculationSpec": { "func": "count", "calculationBy": "entities" }
},
"stale_prs_7d": {
"title": "Stale PRs (7d)",
"type": "number",
"target": "githubPullRequest",
"query": {
"combinator": "and",
"rules": [
{ "property": "status", "operator": "=", "value": "open" },
{ "property": "createdAt", "operator": "notBetween", "value": { "preset": "lastWeek" } }
]
},
"calculationSpec": { "func": "count", "calculationBy": "entities" }
},
"merged_prs_last_week": {
"title": "Merged PRs (Last Week)",
"type": "number",
"target": "githubPullRequest",
"query": {
"combinator": "and",
"rules": [
{ "property": "mergedAt", "operator": "between", "value": { "preset": "lastWeek" } }
]
},
"calculationSpec": { "func": "count", "calculationBy": "entities" }
},
"merged_prs_last_month": {
"title": "Merged PRs (Last Month)",
"type": "number",
"target": "githubPullRequest",
"query": {
"combinator": "and",
"rules": [
{ "property": "mergedAt", "operator": "between", "value": { "preset": "lastMonth" } }
]
},
"calculationSpec": { "func": "count", "calculationBy": "entities" }
},
"pr_cycle_time": {
"title": "Monthly PR Cycle Time",
"type": "number",
"target": "githubPullRequest",
"query": {
"combinator": "and",
"rules": [
{ "property": "mergedAt", "operator": "between", "value": { "preset": "lastMonth" } }
]
},
"calculationSpec": {
"func": "average", "averageOf": "total",
"calculationBy": "property", "property": "cycle_time_hours",
"measureTimeBy": "$createdAt"
}
},
"pr_cycle_time_weekly": {
"title": "Weekly PR Cycle Time",
"type": "number",
"target": "githubPullRequest",
"query": {
"combinator": "and",
"rules": [
{ "property": "mergedAt", "operator": "between", "value": { "preset": "lastWeek" } }
]
},
"calculationSpec": {
"func": "average", "averageOf": "total",
"calculationBy": "property", "property": "cycle_time_hours",
"measureTimeBy": "$createdAt"
}
}Service aggregation properties (click to expand)
"open_prs": {
"title": "Open MRs",
"type": "number",
"target": "gitlabMergeRequest",
"query": {
"combinator": "and",
"rules": [
{ "property": "status", "operator": "=", "value": "opened" }
]
},
"calculationSpec": { "func": "count", "calculationBy": "entities" }
},
"stale_prs_7d": {
"title": "Stale MRs (7d)",
"type": "number",
"target": "gitlabMergeRequest",
"query": {
"combinator": "and",
"rules": [
{ "property": "status", "operator": "=", "value": "opened" },
{ "property": "createdAt", "operator": "notBetween", "value": { "preset": "lastWeek" } }
]
},
"calculationSpec": { "func": "count", "calculationBy": "entities" }
},
"merged_prs_last_week": {
"title": "Weekly MR Throughput",
"type": "number",
"target": "gitlabMergeRequest",
"query": {
"combinator": "and",
"rules": [
{ "property": "mergedAt", "operator": "between", "value": { "preset": "lastWeek" } }
]
},
"calculationSpec": { "func": "count", "calculationBy": "entities" }
},
"merged_prs_last_month": {
"title": "Monthly MR Throughput",
"type": "number",
"target": "gitlabMergeRequest",
"query": {
"combinator": "and",
"rules": [
{ "property": "mergedAt", "operator": "between", "value": { "preset": "lastMonth" } }
]
},
"calculationSpec": { "func": "count", "calculationBy": "entities" }
},
"pr_cycle_time": {
"title": "Monthly MR Cycle Time",
"type": "number",
"target": "gitlabMergeRequest",
"query": {
"combinator": "and",
"rules": [
{ "property": "mergedAt", "operator": "between", "value": { "preset": "lastMonth" } }
]
},
"calculationSpec": {
"func": "average", "averageOf": "total",
"calculationBy": "property", "property": "cycle_time_hours",
"measureTimeBy": "$createdAt"
}
},
"pr_cycle_time_weekly": {
"title": "Weekly MR Cycle Time",
"type": "number",
"target": "gitlabMergeRequest",
"query": {
"combinator": "and",
"rules": [
{ "property": "mergedAt", "operator": "between", "value": { "preset": "lastWeek" } }
]
},
"calculationSpec": {
"func": "average", "averageOf": "total",
"calculationBy": "property", "property": "cycle_time_hours",
"measureTimeBy": "$createdAt"
}
}Service aggregation properties (click to expand)
"open_prs": {
"title": "Open PRs",
"type": "number",
"target": "azureDevopsPullRequest",
"query": {
"combinator": "and",
"rules": [
{ "property": "status", "operator": "=", "value": "active" }
]
},
"calculationSpec": { "func": "count", "calculationBy": "entities" }
},
"stale_prs_7d": {
"title": "Stale PRs (7d)",
"type": "number",
"target": "azureDevopsPullRequest",
"query": {
"combinator": "and",
"rules": [
{ "property": "status", "operator": "=", "value": "active" },
{ "property": "createdAt", "operator": "notBetween", "value": { "preset": "lastWeek" } }
]
},
"calculationSpec": { "func": "count", "calculationBy": "entities" }
},
"merged_prs_last_week": {
"title": "Weekly PR Throughput",
"type": "number",
"target": "azureDevopsPullRequest",
"query": {
"combinator": "and",
"rules": [
{ "property": "status", "operator": "=", "value": "completed" },
{ "property": "$createdAt", "operator": "between", "value": { "preset": "lastWeek" } }
]
},
"calculationSpec": { "func": "count", "calculationBy": "entities" }
},
"merged_prs_last_month": {
"title": "Monthly PR Throughput",
"type": "number",
"target": "azureDevopsPullRequest",
"query": {
"combinator": "and",
"rules": [
{ "property": "status", "operator": "=", "value": "completed" },
{ "property": "$createdAt", "operator": "between", "value": { "preset": "lastMonth" } }
]
},
"calculationSpec": { "func": "count", "calculationBy": "entities" }
},
"pr_cycle_time": {
"title": "Monthly PR Cycle Time",
"type": "number",
"target": "azureDevopsPullRequest",
"query": {
"combinator": "and",
"rules": [
{ "property": "status", "operator": "=", "value": "completed" },
{ "property": "$createdAt", "operator": "between", "value": { "preset": "lastMonth" } }
]
},
"calculationSpec": {
"func": "average", "averageOf": "total",
"calculationBy": "property", "property": "cycle_time_hours",
"measureTimeBy": "$createdAt"
}
},
"pr_cycle_time_weekly": {
"title": "Weekly PR Cycle Time",
"type": "number",
"target": "azureDevopsPullRequest",
"query": {
"combinator": "and",
"rules": [
{ "property": "status", "operator": "=", "value": "completed" },
{ "property": "$createdAt", "operator": "between", "value": { "preset": "lastWeek" } }
]
},
"calculationSpec": {
"func": "average", "averageOf": "total",
"calculationBy": "property", "property": "cycle_time_hours",
"measureTimeBy": "$createdAt"
}
}Azure DevOps throughput filteringAzure DevOps does not have a
mergedAtproperty. Instead, we filter bystatus = "completed"combined with$createdAt(the Port entity creation date) to approximate throughput in the last week/month. -
Add the following entries to the
calculationPropertiessection of the blueprint:Service calculation properties (click to expand)
"stale_pr_share_percent": {
"title": "Stale PR Share (%)",
"type": "number",
"calculation": "if (.properties.open_prs != null and .properties.open_prs != 0) then (.properties.stale_prs_7d / .properties.open_prs) * 100 else 0 end"
},
"cycle_time_trend": {
"title": "Cycle Time Trend",
"type": "string",
"colorized": true,
"colors": {
"Improving": "green",
"Degrading": "red",
"Stable": "blue"
},
"calculation": "((.properties.pr_cycle_time // 0) - (.properties.pr_cycle_time_weekly // 0)) as $diff | if $diff > 0 then \"Improving\" elif $diff < 0 then \"Degrading\" else \"Stable\" end"
},
"throughput_trend": {
"title": "Throughput Trend",
"type": "string",
"colorized": true,
"colors": {
"Improving": "green",
"Degrading": "red",
"Stable": "blue"
},
"calculation": "((.properties.merged_prs_last_week // 0) * 30 - (.properties.merged_prs_last_month // 0) * 7) as $diff | if $diff > 0 then \"Improving\" elif $diff < 0 then \"Degrading\" else \"Stable\" end"
} -
Click Save to update the blueprint.
Update the team blueprint
Add relations, mirror properties, and aggregation properties to the Team blueprint to aggregate delivery metrics across all services owned by each team. The aggregations use pathFilter to traverse the PR → service → team relation chain and compute metrics directly from pull request data.
-
Go to your Builder page.
-
Find the Team blueprint and click on it.
-
Click on the
...button in the top right corner, and choose{...} Edit JSON. . -
Add the following entry to the
relationssection of the blueprint. This self-relation allows teams to be organized into a hierarchy (e.g., a "Frontend" team under an "Engineering" parent), which the dashboard uses to group team metrics by parent team:Team parent relation (click to expand)
"parent_team": {
"title": "Parent Team",
"target": "_team",
"required": false,
"many": false
} -
Add the following entry to the
mirrorPropertiessection of the blueprint. This mirror resolves the parent team's name so the "Team Performance" table can group rows by parent team:Team mirror property (click to expand)
"parent_team_name": {
"title": "Parent Team",
"path": "parent_team.$title"
} -
Add the following entries to the
aggregationPropertiessection of the blueprint:- GitHub
- GitLab
- Azure DevOps
Team aggregation properties (click to expand)
"services_count": {
"title": "Services Count",
"type": "number",
"target": "service",
"calculationSpec": { "func": "count", "calculationBy": "entities" },
"pathFilter": [{ "fromBlueprint": "service", "path": ["team"] }]
},
"open_prs": {
"title": "Open PRs",
"type": "number",
"target": "githubPullRequest",
"query": {
"combinator": "and",
"rules": [{ "property": "status", "operator": "=", "value": "open" }]
},
"calculationSpec": { "func": "count", "calculationBy": "entities" },
"pathFilter": [{ "fromBlueprint": "githubPullRequest", "path": ["service", "team"] }]
},
"stale_prs_7d": {
"title": "Stale PRs (7d)",
"type": "number",
"target": "githubPullRequest",
"query": {
"combinator": "and",
"rules": [
{ "property": "status", "operator": "=", "value": "open" },
{ "property": "createdAt", "operator": "notBetween", "value": { "preset": "lastWeek" } }
]
},
"calculationSpec": { "func": "count", "calculationBy": "entities" },
"pathFilter": [{ "fromBlueprint": "githubPullRequest", "path": ["service", "team"] }]
},
"merged_prs_last_week": {
"title": "Merged PRs (Last Week)",
"type": "number",
"target": "githubPullRequest",
"query": {
"combinator": "and",
"rules": [{ "property": "mergedAt", "operator": "between", "value": { "preset": "lastWeek" } }]
},
"calculationSpec": { "func": "count", "calculationBy": "entities" },
"pathFilter": [{ "fromBlueprint": "githubPullRequest", "path": ["service", "team"] }]
},
"merged_prs_last_month": {
"title": "Merged PRs (Last Month)",
"type": "number",
"target": "githubPullRequest",
"query": {
"combinator": "and",
"rules": [{ "property": "mergedAt", "operator": "between", "value": { "preset": "lastMonth" } }]
},
"calculationSpec": { "func": "count", "calculationBy": "entities" },
"pathFilter": [{ "fromBlueprint": "githubPullRequest", "path": ["service", "team"] }]
},
"pr_cycle_time_weekly": {
"title": "Weekly PR Cycle Time",
"type": "number",
"target": "githubPullRequest",
"query": {
"combinator": "and",
"rules": [{ "property": "mergedAt", "operator": "between", "value": { "preset": "lastWeek" } }]
},
"calculationSpec": {
"func": "average", "averageOf": "total",
"calculationBy": "property", "property": "cycle_time_hours",
"measureTimeBy": "$createdAt"
},
"pathFilter": [{ "fromBlueprint": "githubPullRequest", "path": ["service", "team"] }]
},
"pr_cycle_time": {
"title": "Monthly PR Cycle Time",
"type": "number",
"target": "githubPullRequest",
"query": {
"combinator": "and",
"rules": [{ "property": "mergedAt", "operator": "between", "value": { "preset": "lastMonth" } }]
},
"calculationSpec": {
"func": "average", "averageOf": "total",
"calculationBy": "property", "property": "cycle_time_hours",
"measureTimeBy": "$createdAt"
},
"pathFilter": [{ "fromBlueprint": "githubPullRequest", "path": ["service", "team"] }]
}Team aggregation properties (click to expand)
"services_count": {
"title": "Services Count",
"type": "number",
"target": "service",
"calculationSpec": { "func": "count", "calculationBy": "entities" },
"pathFilter": [{ "fromBlueprint": "service", "path": ["team"] }]
},
"open_prs": {
"title": "Open MRs",
"type": "number",
"target": "gitlabMergeRequest",
"query": {
"combinator": "and",
"rules": [{ "property": "status", "operator": "=", "value": "opened" }]
},
"calculationSpec": { "func": "count", "calculationBy": "entities" },
"pathFilter": [{ "fromBlueprint": "gitlabMergeRequest", "path": ["service", "team"] }]
},
"stale_prs_7d": {
"title": "Stale MRs (7d)",
"type": "number",
"target": "gitlabMergeRequest",
"query": {
"combinator": "and",
"rules": [
{ "property": "status", "operator": "=", "value": "opened" },
{ "property": "createdAt", "operator": "notBetween", "value": { "preset": "lastWeek" } }
]
},
"calculationSpec": { "func": "count", "calculationBy": "entities" },
"pathFilter": [{ "fromBlueprint": "gitlabMergeRequest", "path": ["service", "team"] }]
},
"merged_prs_last_week": {
"title": "Weekly MR Throughput",
"type": "number",
"target": "gitlabMergeRequest",
"query": {
"combinator": "and",
"rules": [{ "property": "mergedAt", "operator": "between", "value": { "preset": "lastWeek" } }]
},
"calculationSpec": { "func": "count", "calculationBy": "entities" },
"pathFilter": [{ "fromBlueprint": "gitlabMergeRequest", "path": ["service", "team"] }]
},
"merged_prs_last_month": {
"title": "Monthly MR Throughput",
"type": "number",
"target": "gitlabMergeRequest",
"query": {
"combinator": "and",
"rules": [{ "property": "mergedAt", "operator": "between", "value": { "preset": "lastMonth" } }]
},
"calculationSpec": { "func": "count", "calculationBy": "entities" },
"pathFilter": [{ "fromBlueprint": "gitlabMergeRequest", "path": ["service", "team"] }]
},
"pr_cycle_time_weekly": {
"title": "Weekly MR Cycle Time",
"type": "number",
"target": "gitlabMergeRequest",
"query": {
"combinator": "and",
"rules": [{ "property": "mergedAt", "operator": "between", "value": { "preset": "lastWeek" } }]
},
"calculationSpec": {
"func": "average", "averageOf": "total",
"calculationBy": "property", "property": "cycle_time_hours",
"measureTimeBy": "$createdAt"
},
"pathFilter": [{ "fromBlueprint": "gitlabMergeRequest", "path": ["service", "team"] }]
},
"pr_cycle_time": {
"title": "Monthly MR Cycle Time",
"type": "number",
"target": "gitlabMergeRequest",
"query": {
"combinator": "and",
"rules": [{ "property": "mergedAt", "operator": "between", "value": { "preset": "lastMonth" } }]
},
"calculationSpec": {
"func": "average", "averageOf": "total",
"calculationBy": "property", "property": "cycle_time_hours",
"measureTimeBy": "$createdAt"
},
"pathFilter": [{ "fromBlueprint": "gitlabMergeRequest", "path": ["service", "team"] }]
}Team aggregation properties (click to expand)
"services_count": {
"title": "Services Count",
"type": "number",
"target": "service",
"calculationSpec": { "func": "count", "calculationBy": "entities" },
"pathFilter": [{ "fromBlueprint": "service", "path": ["team"] }]
},
"open_prs": {
"title": "Open PRs",
"type": "number",
"target": "azureDevopsPullRequest",
"query": {
"combinator": "and",
"rules": [{ "property": "status", "operator": "=", "value": "active" }]
},
"calculationSpec": { "func": "count", "calculationBy": "entities" },
"pathFilter": [{ "fromBlueprint": "azureDevopsPullRequest", "path": ["service", "team"] }]
},
"stale_prs_7d": {
"title": "Stale PRs (7d)",
"type": "number",
"target": "azureDevopsPullRequest",
"query": {
"combinator": "and",
"rules": [
{ "property": "status", "operator": "=", "value": "active" },
{ "property": "createdAt", "operator": "notBetween", "value": { "preset": "lastWeek" } }
]
},
"calculationSpec": { "func": "count", "calculationBy": "entities" },
"pathFilter": [{ "fromBlueprint": "azureDevopsPullRequest", "path": ["service", "team"] }]
},
"merged_prs_last_week": {
"title": "Weekly PR Throughput",
"type": "number",
"target": "azureDevopsPullRequest",
"query": {
"combinator": "and",
"rules": [
{ "property": "status", "operator": "=", "value": "completed" },
{ "property": "$createdAt", "operator": "between", "value": { "preset": "lastWeek" } }
]
},
"calculationSpec": { "func": "count", "calculationBy": "entities" },
"pathFilter": [{ "fromBlueprint": "azureDevopsPullRequest", "path": ["service", "team"] }]
},
"merged_prs_last_month": {
"title": "Monthly PR Throughput",
"type": "number",
"target": "azureDevopsPullRequest",
"query": {
"combinator": "and",
"rules": [
{ "property": "status", "operator": "=", "value": "completed" },
{ "property": "$createdAt", "operator": "between", "value": { "preset": "lastMonth" } }
]
},
"calculationSpec": { "func": "count", "calculationBy": "entities" },
"pathFilter": [{ "fromBlueprint": "azureDevopsPullRequest", "path": ["service", "team"] }]
},
"pr_cycle_time_weekly": {
"title": "Weekly PR Cycle Time",
"type": "number",
"target": "azureDevopsPullRequest",
"query": {
"combinator": "and",
"rules": [
{ "property": "status", "operator": "=", "value": "completed" },
{ "property": "$createdAt", "operator": "between", "value": { "preset": "lastWeek" } }
]
},
"calculationSpec": {
"func": "average", "averageOf": "total",
"calculationBy": "property", "property": "cycle_time_hours",
"measureTimeBy": "$createdAt"
},
"pathFilter": [{ "fromBlueprint": "azureDevopsPullRequest", "path": ["service", "team"] }]
},
"pr_cycle_time": {
"title": "Monthly PR Cycle Time",
"type": "number",
"target": "azureDevopsPullRequest",
"query": {
"combinator": "and",
"rules": [
{ "property": "status", "operator": "=", "value": "completed" },
{ "property": "$createdAt", "operator": "between", "value": { "preset": "lastMonth" } }
]
},
"calculationSpec": {
"func": "average", "averageOf": "total",
"calculationBy": "property", "property": "cycle_time_hours",
"measureTimeBy": "$createdAt"
},
"pathFilter": [{ "fromBlueprint": "azureDevopsPullRequest", "path": ["service", "team"] }]
} -
Add the following entries to the
calculationPropertiessection of the blueprint:Team calculation properties (click to expand)
"stale_pr_share_percent": {
"title": "Stale PR Share (%)",
"type": "number",
"calculation": "if (.properties.open_prs != null and .properties.open_prs != 0) then (.properties.stale_prs_7d / .properties.open_prs) * 100 else 0 end"
},
"cycle_time_trend": {
"title": "Cycle Time Trend",
"type": "string",
"colorized": true,
"colors": {
"Improving": "green",
"Degrading": "red",
"Stable": "blue"
},
"calculation": "((.properties.pr_cycle_time // 0) - (.properties.pr_cycle_time_weekly // 0)) as $diff | if $diff > 0 then \"Improving\" elif $diff < 0 then \"Degrading\" else \"Stable\" end"
},
"throughput_trend": {
"title": "Throughput Trend",
"type": "string",
"colorized": true,
"colors": {
"Improving": "green",
"Degrading": "red",
"Stable": "blue"
},
"calculation": "((.properties.merged_prs_last_week // 0) * 30 - (.properties.merged_prs_last_month // 0) * 7) as $diff | if $diff > 0 then \"Improving\" elif $diff < 0 then \"Degrading\" else \"Stable\" end"
},
"merged_prs_per_service_last_month": {
"title": "Merged PRs per Service (Monthly)",
"type": "number",
"calculation": "if (.properties.services_count != null and .properties.services_count != 0) then (.properties.merged_prs_last_month / .properties.services_count) else 0 end"
},
"open_prs_per_service": {
"title": "Open PRs per Service",
"type": "number",
"calculation": "if (.properties.services_count != null and .properties.services_count != 0) then (.properties.open_prs / .properties.services_count) else 0 end"
},
"stale_prs_per_service_7d": {
"title": "Stale PRs per Service (7d)",
"type": "number",
"calculation": "if (.properties.services_count != null and .properties.services_count != 0) then (.properties.stale_prs_7d / .properties.services_count) else 0 end"
} -
Click Save to update the blueprint.
The aggregation properties on the Team blueprint use a pathFilter that traverses the relation chain PR/MR → service → team. For team-level metrics to populate, each service must have its team relation set to the appropriate team entity.
Update integration mapping
Now we'll update the integration mapping to populate the new properties we added to the PR/MR blueprint. The default mapping already handles core properties. We only need to add mappings for the new fields.
-
Go to your Data Source page.
-
Select your Git integration.
-
Find the pull request / merge request resource block in the mapping and add the additional property mappings:
- GitHub
- GitLab
- Azure DevOps
Additional pull request property mappings (click to expand)
# Add these properties to the first pull-request resource mapping
# alongside the existing properties (status, closedAt, mergedAt, etc.)
reviewDecision: .review_decision
cycle_time_hours: >-
(.created_at as $createdAt | .merged_at as $mergedAt |
($createdAt | sub("\\..*Z$"; "Z") |
strptime("%Y-%m-%dT%H:%M:%SZ") | mktime) as
$createdTimestamp | ($mergedAt | if . == null then null
else sub("\\..*Z$"; "Z") |
strptime("%Y-%m-%dT%H:%M:%SZ") | mktime end) as
$mergedTimestamp | if $mergedTimestamp == null then null
else (((($mergedTimestamp - $createdTimestamp) / 3600) *
100 | floor) / 100) end)
has_assignees: .assignees | length > 0
has_reviewers: .requested_reviewers | length > 0Configure the
pull-requestselector to include closed PRs. By default, the GitHub integration only fetches open pull requests. Including closed PRs ensures that merged PRs are ingested, which is required for cycle time and throughput calculations.For GitHub (Legacy), set
closedPullRequests: truein the selector:- kind: pull-request
selector:
query: 'true'
closedPullRequests: trueFor GitHub (Ocean), use
states,maxResults, andsince:- kind: pull-request
selector:
query: "true"
states: ["open", "closed"]
maxResults: 100
since: 90Ocean property mappingFor GitHub Ocean, map
statusfrom.statein your entity mappings, and use.__repositoryfor the repository relation. See the GitHub Ocean migration guide for details.Merge request property mappings (click to expand)
- kind: merge-request
selector:
query: "true"
states: ["merged", "opened"]
updatedAfter: 90
includeOnlyActiveGroups: true
port:
entity:
mappings:
identifier: .id | tostring
title: .title
blueprint: '"gitlabMergeRequest"'
properties:
status: .state
createdAt: .created_at
updatedAt: .updated_at
mergedAt: .merged_at
link: .web_url
leadTimeHours: >-
(.created_at as $createdAt | .merged_at as $mergedAt |
($createdAt | sub("\\..*Z$"; "Z") |
strptime("%Y-%m-%dT%H:%M:%SZ") | mktime) as
$createdTimestamp | ($mergedAt | if . == null then null
else sub("\\..*Z$"; "Z") |
strptime("%Y-%m-%dT%H:%M:%SZ") | mktime end) as
$mergedTimestamp | if $mergedTimestamp == null then null
else (((($mergedTimestamp - $createdTimestamp) / 3600) *
100 | floor) / 100) end)
cycle_time_hours: >-
(.created_at as $createdAt | .merged_at as $mergedAt |
($createdAt | sub("\\..*Z$"; "Z") |
strptime("%Y-%m-%dT%H:%M:%SZ") | mktime) as
$createdTimestamp | ($mergedAt | if . == null then null
else sub("\\..*Z$"; "Z") |
strptime("%Y-%m-%dT%H:%M:%SZ") | mktime end) as
$mergedTimestamp | if $mergedTimestamp == null then null
else (((($mergedTimestamp - $createdTimestamp) / 3600) *
100 | floor) / 100) end)
has_reviewers: (.reviewers | length) > 0
has_assignees: (.assignees | length) > 0
relations:
repository: .references.full // "" | gsub("!.+"; "")
service: .references.full | gsub("!.+"; "")GitLab merge request statesThe
statesselector includes bothmergedandopenedto ensure that merged MRs are ingested for cycle time and throughput calculations. TheupdatedAfter: 90limits ingestion to MRs updated within the last 90 days.Pull request property mappings (click to expand)
- kind: pull-request
selector:
query: "true"
minTimeInDays: 90
port:
entity:
mappings:
identifier: .repository.id + "/" + (.pullRequestId | tostring)
title: .title
blueprint: '"azureDevopsPullRequest"'
properties:
status: .status
createdAt: .creationDate
description: .description
cycle_time_hours: >-
(.creationDate as $createdAt | .status as $status |
.closedDate as $closedAt |
($createdAt | sub("\\..*Z$"; "Z") |
strptime("%Y-%m-%dT%H:%M:%SZ") | mktime) as
$createdTimestamp | ($closedAt | if . == null then null
else sub("\\..*Z$"; "Z") |
strptime("%Y-%m-%dT%H:%M:%SZ") | mktime end) as
$closedTimestamp | if $status == "completed" and
$closedTimestamp != null then
(((($closedTimestamp - $createdTimestamp) / 3600) *
100 | floor) / 100) else null end)
link: >-
(.url | split("/_apis/")[0]) + "/_git/" +
.repository.name + "/pullrequest/" +
(.pullRequestId | tostring)
has_reviewers: (.reviewers | length) > 0
relations:
repository: .repository.id
service:
combinator: '"and"'
rules:
- operator: '"="'
property: '"ado_repository_id"'
value: .repository.id
azure_devops_reviewers: '[.reviewers[].uniqueName]'
azure_devops_creator: .createdBy.uniqueNameAzure DevOps PR selectorThe
minTimeInDays: 90limits ingestion to PRs from the last 90 days. Azure DevOps does not require a special flag for closed PRs. All PRs (active, completed, abandoned) are fetched by default.
Theservicerelation uses a search relation to match byado_repository_id. -
Click Save & Resync to apply the mapping.
Visualize metrics
We will create a dedicated dashboard to monitor PR delivery metrics using Port's customizable widgets. The dashboard is organized into sections covering high-level metrics, service performance, team performance, stale PR analysis, and PR quality indicators.
Create the dashboard
First, let's create an Engineering Intelligence folder to organize your dashboards, then add the Delivery Performance dashboard inside it:
-
Navigate to your software catalog.
-
Click on the
+button in the left sidebar. -
Select New folder.
-
Name the folder Engineering Intelligence and click Create.
-
Click on the
...near the Engineering Intelligence folder name in the left side bar. -
Choose
+ Create in folderand select New dashboard. -
Name the dashboard Delivery Performance and click Create.
Add widgets
You can populate the dashboard using either an API script or by manually creating each widget through the UI.
- API script
- Manual setup
The fastest way to set up the dashboard is by using Port's API to create all widgets at once.
Get your Port API token
-
In your Port portal, click on your profile picture in the top right corner.
-
Select Credentials.
-
Click Generate API token.
-
Copy the generated token and store it as an environment variable:
export PORT_ACCESS_TOKEN="YOUR_GENERATED_TOKEN"
If your portal is hosted in the EU region, replace api.port.io with api.port-eu.io in the dashboard creation command below.
Create the dashboard with widgets
Save the following JSON (matching your Git provider) to a file named dp_dashboard.json:
- GitHub
- GitLab
- Azure DevOps
Dashboard JSON payload (click to expand)
{
"identifier": "delivery_performance",
"title": "Delivery Performance",
"icon": "Apps",
"type": "dashboard",
"parent": "engineering_intelligence",
"widgets": [
{
"id": "dpDashboardWidget",
"type": "dashboard-widget",
"layout": [
{
"height": 400,
"columns": [
{"id": "avgCycleTime", "size": 4},
{"id": "prCycleTimeTrend", "size": 4},
{"id": "prThroughputTrend", "size": 4}
]
},
{
"height": 400,
"columns": [
{"id": "totalOpenPrs", "size": 4},
{"id": "stalePrShare", "size": 4},
{"id": "teamThroughputBar", "size": 4}
]
},
{
"height": 400,
"columns": [
{"id": "servicePerformance", "size": 12}
]
},
{
"height": 400,
"columns": [
{"id": "teamPerformance", "size": 12}
]
},
{
"height": 484,
"columns": [
{"id": "stalePrTable", "size": 8},
{"id": "prAgeDistribution", "size": 4}
]
},
{
"height": 489,
"columns": [
{"id": "noReviewerTable", "size": 8},
{"id": "reviewerCoverage", "size": 4}
]
},
{
"height": 486,
"columns": [
{"id": "noAssigneeTable", "size": 8},
{"id": "assigneeCoverage", "size": 4}
]
},
{
"height": 400,
"columns": [
{"id": "openPrTable", "size": 12}
]
}
],
"widgets": [
{
"id": "avgCycleTime",
"type": "entities-number-chart",
"title": "Avg PR Cycle Time (Hours)",
"icon": "Metric",
"description": "Average time from PR creation to merge across all services (last month)",
"blueprint": "githubPullRequest",
"chartType": "aggregateByProperty",
"calculationBy": "property",
"func": "average",
"property": "cycle_time_hours",
"averageOf": "total",
"unit": "none",
"dataset": {
"combinator": "and",
"rules": [
{"operator": "between", "property": "mergedAt", "value": {"preset": "lastMonth"}}
]
}
},
{
"id": "totalOpenPrs",
"type": "entities-number-chart",
"title": "Total open PRs",
"icon": "Metric",
"description": "Total number of open PRs across all services in the organisation",
"blueprint": "githubPullRequest",
"chartType": "countEntities",
"calculationBy": "entities",
"func": "count",
"unit": "none",
"dataset": {
"combinator": "and",
"rules": [
{"property": "status", "operator": "=", "value": "open"}
]
}
},
{
"id": "stalePrShare",
"type": "entities-number-chart",
"title": "Stale PR Share (%)",
"icon": "Metric",
"description": "Percentage of open PRs that have been open for more than 7 days",
"blueprint": "_team",
"chartType": "aggregateByProperty",
"calculationBy": "property",
"func": "average",
"property": "stale_pr_share_percent",
"averageOf": "total",
"displayFormatting": "round",
"unit": "none",
"dataset": {"combinator": "and", "rules": []}
},
{
"id": "prCycleTimeTrend",
"type": "line-chart",
"title": "PR Cycle Time Trend (Monthly Trend)",
"icon": "LineChart",
"description": "Displays average time from PR creation to merge over time.",
"blueprint": "githubPullRequest",
"chartType": "aggregatePropertiesValues",
"func": "average",
"properties": ["properties.cycle_time_hours"],
"measureTimeBy": "mergedAt",
"timeInterval": "isoWeek",
"timeRange": {"preset": "lastMonth"},
"xAxisTitle": "Date",
"yAxisTitle": "Cycle Time",
"dataset": {"combinator": "and", "rules": []}
},
{
"id": "prThroughputTrend",
"type": "line-chart",
"title": "PR Throughput (Monthly Trend)",
"icon": "LineChart",
"description": "Shows merged PR volume over time.",
"blueprint": "githubPullRequest",
"chartType": "countEntities",
"func": "count",
"measureTimeBy": "mergedAt",
"timeInterval": "month",
"timeRange": {"preset": "last6Months"},
"xAxisTitle": "Date",
"yAxisTitle": "PRs Merged",
"dataset": {"combinator": "and", "rules": []}
},
{
"id": "teamThroughputBar",
"type": "bar-chart",
"title": "Teams with Highest PR Throughput",
"icon": "Bar",
"description": "Number of PRs merged per team in the last 30 days",
"blueprint": "githubPullRequest",
"property": "mirror-property#team_name",
"dataset": {
"combinator": "and",
"rules": [
{"property": "status", "operator": "=", "value": "merged"},
{"property": "mergedAt", "operator": "between", "value": {"preset": "lastMonth"}}
]
}
},
{
"id": "prAgeDistribution",
"type": "entities-pie-chart",
"title": "PR Age Distribution",
"icon": "Pie",
"description": "PRs opened for: 0-3 days | 3-7 days | 7-30 days | >30 days",
"blueprint": "githubPullRequest",
"property": "calculation-property#pr_age_label",
"dataset": {
"combinator": "and",
"rules": [
{"property": "status", "operator": "=", "value": "open"}
]
}
},
{
"id": "reviewerCoverage",
"type": "entities-pie-chart",
"title": "Open PR Reviewer Coverage",
"icon": "Pie",
"description": "Shows Open PRs with and without assigned reviewers.",
"blueprint": "githubPullRequest",
"property": "property#has_reviewers",
"dataset": {
"combinator": "and",
"rules": [
{"property": "status", "operator": "=", "value": "open"}
]
}
},
{
"id": "assigneeCoverage",
"type": "entities-pie-chart",
"title": "Open PR Assignment Coverage",
"icon": "Pie",
"description": "Shows open PRs with and without assigned owners.",
"blueprint": "githubPullRequest",
"property": "property#has_assignees",
"dataset": {
"combinator": "and",
"rules": [
{"property": "status", "operator": "=", "value": "open"}
]
}
},
{
"id": "servicePerformance",
"type": "table-entities-explorer",
"displayMode": "widget",
"title": "Service Performance Overview",
"icon": "Table",
"description": "Delivery metrics across services",
"blueprint": "service",
"dataset": {"combinator": "and", "rules": []},
"excludedFields": [],
"blueprintConfig": {
"service": {
"groupSettings": {"groupBy": ["parent_team_name", "team"]},
"propertiesSettings": {
"order": ["team", "parent_team_name", "$icon", "$title", "delivery_performance", "pr_cycle_time", "cycle_time_trend", "merged_prs_last_month", "throughput_trend", "stale_pr_share_percent", "open_prs", "stale_prs_7d", "FROZEN_RIGHT_COLUMN"],
"shown": ["$title", "readme", "parent_team_name", "cycle_time_trend", "stale_pr_share_percent", "throughput_trend", "merged_prs_last_month", "open_prs", "pr_cycle_time", "stale_prs_7d", "delivery_performance", "team"]
},
"filterSettings": {"filterBy": {"combinator": "and", "rules": []}},
"sortSettings": {"sortBy": []}
}
}
},
{
"id": "teamPerformance",
"type": "table-entities-explorer",
"displayMode": "widget",
"title": "Team Performance",
"icon": "Table",
"description": "Delivery metrics aggregated per team",
"blueprint": "_team",
"dataset": {"combinator": "and", "rules": []},
"excludedFields": [],
"blueprintConfig": {
"_team": {
"groupSettings": {"groupBy": ["parent_team_name"]},
"propertiesSettings": {
"order": ["$title", "merged_prs_last_month", "throughput_trend", "pr_cycle_time", "cycle_time_trend", "stale_pr_share_percent", "open_prs", "stale_prs_7d", "services_count"],
"shown": ["$title", "stale_pr_share_percent", "services_count", "open_prs", "stale_prs_7d", "merged_prs_last_month", "throughput_trend", "pr_cycle_time", "cycle_time_trend"]
},
"filterSettings": {"filterBy": {"combinator": "and", "rules": []}},
"sortSettings": {"sortBy": []}
}
}
},
{
"id": "stalePrTable",
"type": "table-entities-explorer",
"displayMode": "widget",
"title": "Stale Pull Requests",
"icon": "Table",
"description": "Pull requests that have been open for more than 7 days",
"blueprint": "githubPullRequest",
"dataset": {"combinator": "and", "rules": []},
"excludedFields": [],
"blueprintConfig": {
"githubPullRequest": {
"groupSettings": {"groupBy": ["team_name", "service", "repository"]},
"propertiesSettings": {
"order": ["repository", "service", "team_name", "link", "$title", "$updatedAt", "git_hub_creator"],
"shown": ["$updatedAt", "$title", "link", "git_hub_creator"]
},
"filterSettings": {
"filterBy": {
"combinator": "and",
"rules": [
{"property": "is_stale", "operator": "=", "value": true}
]
}
},
"sortSettings": {"sortBy": []}
}
}
},
{
"id": "openPrTable",
"type": "table-entities-explorer",
"displayMode": "widget",
"title": "All Open Pull Requests",
"icon": "Table",
"description": "All currently open pull requests across repositories",
"blueprint": "githubPullRequest",
"dataset": {
"combinator": "and",
"rules": [
{"property": "status", "operator": "=", "value": "open"}
]
},
"excludedFields": [],
"blueprintConfig": {
"githubPullRequest": {
"groupSettings": {"groupBy": ["team_name", "service", "repository"]},
"propertiesSettings": {
"order": ["repository", "service", "team_name", "link", "$title", "git_hub_creator", "$updatedAt", "days_old"],
"shown": ["$updatedAt", "$title", "has_assignees", "has_reviewers", "link", "team_name", "days_old", "is_stale", "service", "git_hub_creator"]
},
"filterSettings": {"filterBy": {"combinator": "and", "rules": []}},
"sortSettings": {"sortBy": []}
}
}
},
{
"id": "noReviewerTable",
"type": "table-entities-explorer",
"displayMode": "widget",
"title": "PRs Without Assigned Reviewers",
"icon": "Table",
"description": "Lists pull requests that currently have no reviewers assigned.",
"blueprint": "githubPullRequest",
"dataset": {
"combinator": "and",
"rules": [
{"property": "status", "operator": "=", "value": "open"},
{"property": "has_reviewers", "operator": "=", "value": false}
]
},
"excludedFields": [],
"blueprintConfig": {
"githubPullRequest": {
"groupSettings": {"groupBy": ["team_name", "service", "repository"]},
"propertiesSettings": {
"order": ["repository", "service", "team_name", "link", "$title", "git_hub_creator", "$updatedAt", "days_old"],
"shown": ["$updatedAt", "$title", "link", "days_old", "git_hub_creator"]
},
"filterSettings": {"filterBy": {"combinator": "and", "rules": []}},
"sortSettings": {"sortBy": [{"property": "$identifier", "order": "asc"}]}
}
}
},
{
"id": "noAssigneeTable",
"type": "table-entities-explorer",
"displayMode": "widget",
"title": "PRs Without Assignees",
"icon": "Table",
"description": "Lists pull requests that currently have no assignees assigned.",
"blueprint": "githubPullRequest",
"dataset": {
"combinator": "and",
"rules": [
{"property": "status", "operator": "=", "value": "open"},
{"property": "has_assignees", "operator": "=", "value": false}
]
},
"excludedFields": [],
"blueprintConfig": {
"githubPullRequest": {
"groupSettings": {"groupBy": ["team_name", "service", "repository"]},
"propertiesSettings": {
"order": ["repository", "service", "team_name", "$icon", "link", "$title", "git_hub_creator", "$updatedAt", "days_old", "FROZEN_RIGHT_COLUMN"],
"shown": ["$updatedAt", "$title", "link", "days_old", "git_hub_creator"]
},
"filterSettings": {"filterBy": {"combinator": "and", "rules": []}},
"sortSettings": {"sortBy": [{"property": "$identifier", "order": "asc"}]}
}
}
}
]
}
]
}
Dashboard JSON payload (click to expand)
{
"identifier": "delivery_performance",
"title": "Delivery Performance",
"icon": "Apps",
"type": "dashboard",
"parent": "engineering_intelligence",
"widgets": [
{
"id": "dpDashboardWidget",
"type": "dashboard-widget",
"layout": [
{
"height": 400,
"columns": [
{"id": "avgCycleTime", "size": 4},
{"id": "prCycleTimeTrend", "size": 4},
{"id": "prThroughputTrend", "size": 4}
]
},
{
"height": 400,
"columns": [
{"id": "totalOpenPrs", "size": 4},
{"id": "stalePrShare", "size": 4},
{"id": "teamThroughputBar", "size": 4}
]
},
{
"height": 400,
"columns": [
{"id": "servicePerformance", "size": 12}
]
},
{
"height": 400,
"columns": [
{"id": "teamPerformance", "size": 12}
]
},
{
"height": 484,
"columns": [
{"id": "stalePrTable", "size": 8},
{"id": "prAgeDistribution", "size": 4}
]
},
{
"height": 489,
"columns": [
{"id": "noReviewerTable", "size": 8},
{"id": "reviewerCoverage", "size": 4}
]
},
{
"height": 486,
"columns": [
{"id": "noAssigneeTable", "size": 8},
{"id": "assigneeCoverage", "size": 4}
]
},
{
"height": 400,
"columns": [
{"id": "openPrTable", "size": 12}
]
}
],
"widgets": [
{
"id": "avgCycleTime",
"type": "entities-number-chart",
"title": "Avg PR Cycle Time (Hours)",
"icon": "Metric",
"description": "Average time from PR creation to merge across all services (last month)",
"blueprint": "gitlabMergeRequest",
"chartType": "aggregateByProperty",
"calculationBy": "property",
"func": "average",
"property": "cycle_time_hours",
"averageOf": "total",
"unit": "none",
"dataset": {
"combinator": "and",
"rules": [
{"operator": "between", "property": "mergedAt", "value": {"preset": "lastMonth"}}
]
}
},
{
"id": "totalOpenPrs",
"type": "entities-number-chart",
"title": "Total open MRs",
"icon": "Metric",
"description": "Total number of open MRs across all services in the organisation",
"blueprint": "gitlabMergeRequest",
"chartType": "countEntities",
"calculationBy": "entities",
"func": "count",
"unit": "none",
"dataset": {
"combinator": "and",
"rules": [
{"property": "status", "operator": "=", "value": "opened"}
]
}
},
{
"id": "stalePrShare",
"type": "entities-number-chart",
"title": "Stale MR Share (%)",
"icon": "Metric",
"description": "Percentage of open MRs that have been open for more than 7 days",
"blueprint": "_team",
"chartType": "aggregateByProperty",
"calculationBy": "property",
"func": "average",
"property": "stale_pr_share_percent",
"averageOf": "total",
"displayFormatting": "round",
"unit": "none",
"dataset": {"combinator": "and", "rules": []}
},
{
"id": "prCycleTimeTrend",
"type": "line-chart",
"title": "MR Cycle Time Trend (Monthly Trend)",
"icon": "LineChart",
"description": "Displays average time from MR creation to merge over time.",
"blueprint": "gitlabMergeRequest",
"chartType": "aggregatePropertiesValues",
"func": "average",
"properties": ["properties.cycle_time_hours"],
"measureTimeBy": "mergedAt",
"timeInterval": "isoWeek",
"timeRange": {"preset": "lastMonth"},
"xAxisTitle": "Date",
"yAxisTitle": "Cycle Time",
"dataset": {"combinator": "and", "rules": []}
},
{
"id": "prThroughputTrend",
"type": "line-chart",
"title": "MR Throughput (Monthly Trend)",
"icon": "LineChart",
"description": "Shows merged MR volume over time.",
"blueprint": "gitlabMergeRequest",
"chartType": "countEntities",
"func": "count",
"measureTimeBy": "mergedAt",
"timeInterval": "month",
"timeRange": {"preset": "last6Months"},
"xAxisTitle": "Date",
"yAxisTitle": "MRs Merged",
"dataset": {"combinator": "and", "rules": []}
},
{
"id": "teamThroughputBar",
"type": "bar-chart",
"title": "Teams with Highest MR Throughput",
"icon": "Bar",
"description": "Number of MRs merged per team in the last 30 days",
"blueprint": "gitlabMergeRequest",
"property": "mirror-property#team_name",
"dataset": {
"combinator": "and",
"rules": [
{"property": "status", "operator": "=", "value": "merged"},
{"property": "mergedAt", "operator": "between", "value": {"preset": "lastMonth"}}
]
}
},
{
"id": "prAgeDistribution",
"type": "entities-pie-chart",
"title": "MR Age Distribution",
"icon": "Pie",
"description": "MRs opened for: 0-3 days | 3-7 days | 7-30 days | >30 days",
"blueprint": "gitlabMergeRequest",
"property": "calculation-property#pr_age_label",
"dataset": {
"combinator": "and",
"rules": [
{"property": "status", "operator": "=", "value": "opened"}
]
}
},
{
"id": "reviewerCoverage",
"type": "entities-pie-chart",
"title": "Open MR Reviewer Coverage",
"icon": "Pie",
"description": "Shows open MRs with and without assigned reviewers.",
"blueprint": "gitlabMergeRequest",
"property": "property#has_reviewers",
"dataset": {
"combinator": "and",
"rules": [
{"property": "status", "operator": "=", "value": "opened"}
]
}
},
{
"id": "assigneeCoverage",
"type": "entities-pie-chart",
"title": "Open MR Assignment Coverage",
"icon": "Pie",
"description": "Shows open MRs with and without assigned owners.",
"blueprint": "gitlabMergeRequest",
"property": "property#has_assignees",
"dataset": {
"combinator": "and",
"rules": [
{"property": "status", "operator": "=", "value": "opened"}
]
}
},
{
"id": "servicePerformance",
"type": "table-entities-explorer",
"displayMode": "widget",
"title": "Service Performance Overview",
"icon": "Table",
"description": "Delivery metrics across services",
"blueprint": "service",
"dataset": {"combinator": "and", "rules": []},
"excludedFields": [],
"blueprintConfig": {
"service": {
"groupSettings": {"groupBy": ["parent_team_name", "team"]},
"propertiesSettings": {
"order": ["team", "parent_team_name", "$icon", "$title", "delivery_performance", "pr_cycle_time", "cycle_time_trend", "merged_prs_last_month", "throughput_trend", "stale_pr_share_percent", "open_prs", "stale_prs_7d", "FROZEN_RIGHT_COLUMN"],
"shown": ["$title", "readme", "parent_team_name", "cycle_time_trend", "stale_pr_share_percent", "throughput_trend", "merged_prs_last_month", "open_prs", "pr_cycle_time", "stale_prs_7d", "delivery_performance", "team"]
},
"filterSettings": {"filterBy": {"combinator": "and", "rules": []}},
"sortSettings": {"sortBy": []}
}
}
},
{
"id": "teamPerformance",
"type": "table-entities-explorer",
"displayMode": "widget",
"title": "Team Performance",
"icon": "Table",
"description": "Delivery metrics aggregated per team",
"blueprint": "_team",
"dataset": {"combinator": "and", "rules": []},
"excludedFields": [],
"blueprintConfig": {
"_team": {
"groupSettings": {"groupBy": ["parent_team_name"]},
"propertiesSettings": {
"order": ["$title", "merged_prs_last_month", "throughput_trend", "pr_cycle_time", "cycle_time_trend", "stale_pr_share_percent", "open_prs", "stale_prs_7d", "services_count"],
"shown": ["$title", "stale_pr_share_percent", "services_count", "open_prs", "stale_prs_7d", "merged_prs_last_month", "throughput_trend", "pr_cycle_time", "cycle_time_trend"]
},
"filterSettings": {"filterBy": {"combinator": "and", "rules": []}},
"sortSettings": {"sortBy": []}
}
}
},
{
"id": "stalePrTable",
"type": "table-entities-explorer",
"displayMode": "widget",
"title": "Stale Merge Requests",
"icon": "Table",
"description": "Merge requests that have been open for more than 7 days",
"blueprint": "gitlabMergeRequest",
"dataset": {"combinator": "and", "rules": []},
"excludedFields": [],
"blueprintConfig": {
"gitlabMergeRequest": {
"groupSettings": {"groupBy": ["team_name", "service", "repository"]},
"propertiesSettings": {
"order": ["repository", "service", "team_name", "link", "$title", "$updatedAt", "creator"],
"shown": ["$updatedAt", "$title", "link", "creator"]
},
"filterSettings": {
"filterBy": {
"combinator": "and",
"rules": [
{"property": "is_stale", "operator": "=", "value": true}
]
}
},
"sortSettings": {"sortBy": []}
}
}
},
{
"id": "openPrTable",
"type": "table-entities-explorer",
"displayMode": "widget",
"title": "All Open Merge Requests",
"icon": "Table",
"description": "All currently open merge requests across repositories",
"blueprint": "gitlabMergeRequest",
"dataset": {
"combinator": "and",
"rules": [
{"property": "status", "operator": "=", "value": "opened"}
]
},
"excludedFields": [],
"blueprintConfig": {
"gitlabMergeRequest": {
"groupSettings": {"groupBy": ["team_name", "service", "repository"]},
"propertiesSettings": {
"order": ["repository", "service", "team_name", "link", "$title", "creator", "$updatedAt", "days_old"],
"shown": ["$updatedAt", "$title", "has_assignees", "has_reviewers", "link", "team_name", "days_old", "is_stale", "service", "creator"]
},
"filterSettings": {"filterBy": {"combinator": "and", "rules": []}},
"sortSettings": {"sortBy": []}
}
}
},
{
"id": "noReviewerTable",
"type": "table-entities-explorer",
"displayMode": "widget",
"title": "MRs Without Assigned Reviewers",
"icon": "Table",
"description": "Lists merge requests that currently have no reviewers assigned.",
"blueprint": "gitlabMergeRequest",
"dataset": {
"combinator": "and",
"rules": [
{"property": "status", "operator": "=", "value": "opened"},
{"property": "has_reviewers", "operator": "=", "value": false}
]
},
"excludedFields": [],
"blueprintConfig": {
"gitlabMergeRequest": {
"groupSettings": {"groupBy": ["team_name", "service", "repository"]},
"propertiesSettings": {
"order": ["repository", "service", "team_name", "link", "$title", "creator", "$updatedAt", "days_old"],
"shown": ["$updatedAt", "$title", "link", "days_old", "creator"]
},
"filterSettings": {"filterBy": {"combinator": "and", "rules": []}},
"sortSettings": {"sortBy": [{"property": "$identifier", "order": "asc"}]}
}
}
},
{
"id": "noAssigneeTable",
"type": "table-entities-explorer",
"displayMode": "widget",
"title": "MRs Without Assignees",
"icon": "Table",
"description": "Lists merge requests that currently have no assignees assigned.",
"blueprint": "gitlabMergeRequest",
"dataset": {
"combinator": "and",
"rules": [
{"property": "status", "operator": "=", "value": "opened"},
{"property": "has_assignees", "operator": "=", "value": false}
]
},
"excludedFields": [],
"blueprintConfig": {
"gitlabMergeRequest": {
"groupSettings": {"groupBy": ["team_name", "service", "repository"]},
"propertiesSettings": {
"order": ["repository", "service", "team_name", "$icon", "link", "$title", "creator", "$updatedAt", "days_old", "FROZEN_RIGHT_COLUMN"],
"shown": ["$updatedAt", "$title", "link", "days_old", "creator"]
},
"filterSettings": {"filterBy": {"combinator": "and", "rules": []}},
"sortSettings": {"sortBy": [{"property": "$identifier", "order": "asc"}]}
}
}
}
]
}
]
}
Dashboard JSON payload (click to expand)
{
"identifier": "delivery_performance",
"title": "Delivery Performance",
"icon": "Apps",
"type": "dashboard",
"parent": "engineering_intelligence",
"widgets": [
{
"id": "dpDashboardWidget",
"type": "dashboard-widget",
"layout": [
{
"height": 400,
"columns": [
{"id": "avgCycleTime", "size": 4},
{"id": "prCycleTimeTrend", "size": 4},
{"id": "prThroughputTrend", "size": 4}
]
},
{
"height": 400,
"columns": [
{"id": "totalOpenPrs", "size": 4},
{"id": "stalePrShare", "size": 4},
{"id": "teamThroughputBar", "size": 4}
]
},
{
"height": 400,
"columns": [
{"id": "servicePerformance", "size": 12}
]
},
{
"height": 400,
"columns": [
{"id": "teamPerformance", "size": 12}
]
},
{
"height": 484,
"columns": [
{"id": "stalePrTable", "size": 8},
{"id": "prAgeDistribution", "size": 4}
]
},
{
"height": 489,
"columns": [
{"id": "noReviewerTable", "size": 8},
{"id": "reviewerCoverage", "size": 4}
]
},
{
"height": 400,
"columns": [
{"id": "openPrTable", "size": 12}
]
}
],
"widgets": [
{
"id": "avgCycleTime",
"type": "entities-number-chart",
"title": "Avg PR Cycle Time (Hours)",
"icon": "Metric",
"description": "Average time from PR creation to merge across all services (last month)",
"blueprint": "azureDevopsPullRequest",
"chartType": "aggregateByProperty",
"calculationBy": "property",
"func": "average",
"property": "cycle_time_hours",
"averageOf": "total",
"unit": "none",
"dataset": {
"combinator": "and",
"rules": [
{"operator": "between", "property": "closedDate", "value": {"preset": "lastMonth"}}
]
}
},
{
"id": "totalOpenPrs",
"type": "entities-number-chart",
"title": "Total open PRs",
"icon": "Metric",
"description": "Total number of open PRs across all services in the organisation",
"blueprint": "azureDevopsPullRequest",
"chartType": "countEntities",
"calculationBy": "entities",
"func": "count",
"unit": "none",
"dataset": {
"combinator": "and",
"rules": [
{"property": "status", "operator": "=", "value": "active"}
]
}
},
{
"id": "stalePrShare",
"type": "entities-number-chart",
"title": "Stale PR Share (%)",
"icon": "Metric",
"description": "Percentage of open PRs that have been open for more than 7 days",
"blueprint": "_team",
"chartType": "aggregateByProperty",
"calculationBy": "property",
"func": "average",
"property": "stale_pr_share_percent",
"averageOf": "total",
"displayFormatting": "round",
"unit": "none",
"dataset": {"combinator": "and", "rules": []}
},
{
"id": "prCycleTimeTrend",
"type": "line-chart",
"title": "PR Cycle Time Trend (Monthly Trend)",
"icon": "LineChart",
"description": "Displays average time from PR creation to completion over time.",
"blueprint": "azureDevopsPullRequest",
"chartType": "aggregatePropertiesValues",
"func": "average",
"properties": ["properties.cycle_time_hours"],
"measureTimeBy": "closedDate",
"timeInterval": "isoWeek",
"timeRange": {"preset": "lastMonth"},
"xAxisTitle": "Date",
"yAxisTitle": "Cycle Time",
"dataset": {"combinator": "and", "rules": []}
},
{
"id": "prThroughputTrend",
"type": "line-chart",
"title": "PR Throughput (Monthly Trend)",
"icon": "LineChart",
"description": "Shows completed PR volume over time.",
"blueprint": "azureDevopsPullRequest",
"chartType": "countEntities",
"func": "count",
"measureTimeBy": "closedDate",
"timeInterval": "month",
"timeRange": {"preset": "last6Months"},
"xAxisTitle": "Date",
"yAxisTitle": "PRs Completed",
"dataset": {"combinator": "and", "rules": []}
},
{
"id": "teamThroughputBar",
"type": "bar-chart",
"title": "Teams with Highest PR Throughput",
"icon": "Bar",
"description": "Number of PRs completed per team in the last 30 days",
"blueprint": "azureDevopsPullRequest",
"property": "mirror-property#team_name",
"dataset": {
"combinator": "and",
"rules": [
{"property": "status", "operator": "=", "value": "completed"},
{"property": "closedDate", "operator": "between", "value": {"preset": "lastMonth"}}
]
}
},
{
"id": "prAgeDistribution",
"type": "entities-pie-chart",
"title": "PR Age Distribution",
"icon": "Pie",
"description": "PRs opened for: 0-3 days | 3-7 days | 7-30 days | >30 days",
"blueprint": "azureDevopsPullRequest",
"property": "calculation-property#pr_age_label",
"dataset": {
"combinator": "and",
"rules": [
{"property": "status", "operator": "=", "value": "active"}
]
}
},
{
"id": "reviewerCoverage",
"type": "entities-pie-chart",
"title": "Open PR Reviewer Coverage",
"icon": "Pie",
"description": "Shows open PRs with and without assigned reviewers.",
"blueprint": "azureDevopsPullRequest",
"property": "property#has_reviewers",
"dataset": {
"combinator": "and",
"rules": [
{"property": "status", "operator": "=", "value": "active"}
]
}
},
{
"id": "servicePerformance",
"type": "table-entities-explorer",
"displayMode": "widget",
"title": "Service Performance Overview",
"icon": "Table",
"description": "Delivery metrics across services",
"blueprint": "service",
"dataset": {"combinator": "and", "rules": []},
"excludedFields": [],
"blueprintConfig": {
"service": {
"groupSettings": {"groupBy": ["parent_team_name", "team"]},
"propertiesSettings": {
"order": ["team", "parent_team_name", "$icon", "$title", "delivery_performance", "pr_cycle_time", "cycle_time_trend", "merged_prs_last_month", "throughput_trend", "stale_pr_share_percent", "open_prs", "stale_prs_7d", "FROZEN_RIGHT_COLUMN"],
"shown": ["$title", "readme", "parent_team_name", "cycle_time_trend", "stale_pr_share_percent", "throughput_trend", "merged_prs_last_month", "open_prs", "pr_cycle_time", "stale_prs_7d", "delivery_performance", "team"]
},
"filterSettings": {"filterBy": {"combinator": "and", "rules": []}},
"sortSettings": {"sortBy": []}
}
}
},
{
"id": "teamPerformance",
"type": "table-entities-explorer",
"displayMode": "widget",
"title": "Team Performance",
"icon": "Table",
"description": "Delivery metrics aggregated per team",
"blueprint": "_team",
"dataset": {"combinator": "and", "rules": []},
"excludedFields": [],
"blueprintConfig": {
"_team": {
"groupSettings": {"groupBy": ["parent_team_name"]},
"propertiesSettings": {
"order": ["$title", "merged_prs_last_month", "throughput_trend", "pr_cycle_time", "cycle_time_trend", "stale_pr_share_percent", "open_prs", "stale_prs_7d", "services_count"],
"shown": ["$title", "stale_pr_share_percent", "services_count", "open_prs", "stale_prs_7d", "merged_prs_last_month", "throughput_trend", "pr_cycle_time", "cycle_time_trend"]
},
"filterSettings": {"filterBy": {"combinator": "and", "rules": []}},
"sortSettings": {"sortBy": []}
}
}
},
{
"id": "stalePrTable",
"type": "table-entities-explorer",
"displayMode": "widget",
"title": "Stale Pull Requests",
"icon": "Table",
"description": "Pull requests that have been open for more than 7 days",
"blueprint": "azureDevopsPullRequest",
"dataset": {"combinator": "and", "rules": []},
"excludedFields": [],
"blueprintConfig": {
"azureDevopsPullRequest": {
"groupSettings": {"groupBy": ["team_name", "service", "repository"]},
"propertiesSettings": {
"order": ["repository", "service", "team_name", "link", "$title", "$updatedAt", "azure_devops_creator"],
"shown": ["$updatedAt", "$title", "link", "azure_devops_creator"]
},
"filterSettings": {
"filterBy": {
"combinator": "and",
"rules": [
{"property": "is_stale", "operator": "=", "value": true}
]
}
},
"sortSettings": {"sortBy": []}
}
}
},
{
"id": "openPrTable",
"type": "table-entities-explorer",
"displayMode": "widget",
"title": "All Open Pull Requests",
"icon": "Table",
"description": "All currently open pull requests across repositories",
"blueprint": "azureDevopsPullRequest",
"dataset": {
"combinator": "and",
"rules": [
{"property": "status", "operator": "=", "value": "active"}
]
},
"excludedFields": [],
"blueprintConfig": {
"azureDevopsPullRequest": {
"groupSettings": {"groupBy": ["team_name", "service", "repository"]},
"propertiesSettings": {
"order": ["repository", "service", "team_name", "link", "$title", "azure_devops_creator", "$updatedAt", "days_old"],
"shown": ["$updatedAt", "$title", "has_reviewers", "link", "team_name", "days_old", "is_stale", "service", "azure_devops_creator"]
},
"filterSettings": {"filterBy": {"combinator": "and", "rules": []}},
"sortSettings": {"sortBy": []}
}
}
},
{
"id": "noReviewerTable",
"type": "table-entities-explorer",
"displayMode": "widget",
"title": "PRs Without Assigned Reviewers",
"icon": "Table",
"description": "Lists pull requests that currently have no reviewers assigned.",
"blueprint": "azureDevopsPullRequest",
"dataset": {
"combinator": "and",
"rules": [
{"property": "status", "operator": "=", "value": "active"},
{"property": "has_reviewers", "operator": "=", "value": false}
]
},
"excludedFields": [],
"blueprintConfig": {
"azureDevopsPullRequest": {
"groupSettings": {"groupBy": ["team_name", "service", "repository"]},
"propertiesSettings": {
"order": ["repository", "service", "team_name", "link", "$title", "azure_devops_creator", "$updatedAt", "days_old"],
"shown": ["$updatedAt", "$title", "link", "days_old", "azure_devops_creator"]
},
"filterSettings": {"filterBy": {"combinator": "and", "rules": []}},
"sortSettings": {"sortBy": [{"property": "$identifier", "order": "asc"}]}
}
}
}
]
}
]
}
Then run the following command to create the dashboard with all widgets:
curl -s -X POST "https://api.port.io/v1/pages" \
-H "Authorization: Bearer $PORT_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d @dp_dashboard.json | python3 -m json.tool
The script assumes an engineering_intelligence folder already exists in your catalog. If you haven't created it yet, follow step 1-4 in the create the dashboard section first else you will run into an error when you run the script
High-level PR metrics
Start with summary widgets that give an at-a-glance view of PR delivery health. Click Title: Description: Select Select Select Select Add this JSON to the Additional filters editor: Select Click Click Title: Description: Set X axis: Set Y axis title: Click + Line and configure: Click Click Title: Description: Set X axis: Set Y axis title: Click + Line and configure: Click Click Title: Description: Select Select Add this JSON to the Dataset filter editor: Click Click Title: Description: Select Select Select Select Select Click Click Title: Description: Select your PR/MR blueprint (GitHub Pull Request, Merge Request, or Pull Request). Select Add this JSON to the Additional filters editor: Click Avg PR cycle time (hours) (click to expand)
+ Widget and select Number Chart.Avg PR Cycle Time (Hours).Average time from PR creation to merge across all services.Aggregate Property (All Entities) Chart type and choose your PR/MR blueprint (GitHub Pull Request, Merge Request, or Pull Request).cycle_time_hours as the Property.average for the Function.total for Average of.{
"combinator": "and",
"rules": [
{
"property": "mergedAt",
"operator": "between",
"value": { "preset": "lastMonth" }
}
]
}{
"combinator": "and",
"rules": [
{
"property": "mergedAt",
"operator": "between",
"value": { "preset": "lastMonth" }
}
]
}{
"combinator": "and",
"rules": [
{
"property": "closedDate",
"operator": "between",
"value": { "preset": "lastMonth" }
}
]
}custom as the Unit and input hours as the Custom unit.Save.PR cycle time trend (monthly) (click to expand)
+ Widget and select Line Chart.PR Cycle Time Trend (Monthly Trend).Displays average time from PR creation to merge over time.
Date.Week.In the past 30 days.Cycle Time.
PR cycle time.Aggregate by property.GitHub Pull Request.PR cycle time (hours).Average.mergedAt.Save.PR throughput (monthly trend) (click to expand)
+ Widget and select Line Chart.PR Throughput (Monthly Trend).Shows merged PR volume over time.
Date.Month.In the past 180 days.PRs Merged.
PRs merged.Count entities.GitHub Pull Request.Count.mergedAt.Save.Total open PRs (click to expand)
+ Widget and select Number Chart.Total open PRs.Total number of open PRs across all services in the organisation.Count entities Chart type and choose your PR/MR blueprint (GitHub Pull Request, Merge Request, or Pull Request).count for the Function.{
"combinator": "and",
"rules": [
{ "property": "status", "operator": "=", "value": "open" }
]
}{
"combinator": "and",
"rules": [
{ "property": "status", "operator": "=", "value": "opened" }
]
}{
"combinator": "and",
"rules": [
{ "property": "status", "operator": "=", "value": "active" }
]
}Save.Stale PR share (%) (click to expand)
+ Widget and select Number Chart.Stale PR Share (%).Percentage of open PRs that have been open for more than 7 days.Aggregate Property (All Entities) Chart type and choose Team as the Blueprint.Stale PR Share (%) as the Property.average for the Function.total for Average of.custom as the Unit and input % as the unit and align the unit to the right.Save.Teams with highest PR throughput (click to expand)
+ Widget and select Bar Chart.Teams with Highest PR Throughput.Number of PRs merged per team in the last 30 days.Team as the Breakdown by property.{
"combinator": "and",
"rules": [
{ "property": "status", "operator": "=", "value": "merged" },
{ "property": "mergedAt", "operator": "between", "value": { "preset": "lastMonth" } }
]
}{
"combinator": "and",
"rules": [
{ "property": "status", "operator": "=", "value": "merged" },
{ "property": "mergedAt", "operator": "between", "value": { "preset": "lastMonth" } }
]
}{
"combinator": "and",
"rules": [
{ "property": "status", "operator": "=", "value": "completed" },
{ "property": "closedDate", "operator": "between", "value": { "preset": "lastMonth" } }
]
}Save.
Service performance
Track delivery metrics at the individual service level using a table that aggregates key PR metrics per service. Click Title: Description: Choose the Service blueprint. Click Click on the In the top right corner of the table, click on Manage Properties and add the following columns: Select Click on the save icon in the top right corner of the widget to save the customized table.Service performance overview (click to expand)
+ Widget and select Table.Service Performance Overview.Delivery metrics across services.Save.... button in the top right corner of the table and select Customize table.
pr_cycle_time): Average cycle time across PRs in the last month.cycle_time_trend): Sparkline showing cycle time direction.merged_prs_last_month): Number of PRs merged in the last 30 days.throughput_trend): Sparkline showing throughput direction.stale_pr_share_percent): Percentage of open PRs that are stale.open_prs): Number of currently open PRs.parent_team_name and team as the Group by column.
Team performance
Aggregate delivery metrics at the team level to compare performance across teams. Click Title: Description: Choose the Team blueprint. Click Click on the In the top right corner of the table, click on Manage Properties and add the following columns: Select Click on the save icon in the top right corner of the widget to save the customized table.Team performance (click to expand)
+ Widget and select Table.Team Performance.Delivery metrics aggregated per team.Save.... button in the top right corner of the table and select Customize table.
merged_prs_last_month): Number of PRs merged in the last 30 days.throughput_trend): Whether throughput is improving, degrading, or stable.pr_cycle_time): Average PR cycle time for the last month.cycle_time_trend): Whether cycle time is improving, degrading, or stable.stale_pr_share_percent): Percentage of open PRs that are stale.open_prs): Number of currently open PRs.stale_prs_7d): Number of PRs open longer than 7 days.services_count): Number of services owned by the team.parent_team_name as the Group by column.
Stale PR analysis
Surface PRs that have been open too long and understand the age distribution across your organization. Click Title: Description: Choose the GitHub Pull Request blueprint. Click Click on the In the top right corner of the table, click on Manage Properties and add the following columns: Select On the filter section, select the Click on the save icon in the top right corner of the widget to save the customized table. Click Title: Description: Choose your PR/MR blueprint (GitHub Pull Request, Merge Request, or Pull Request). Under Breakdown by property, select the Add this JSON to the Additional filters editor: Click Stale pull requests table (click to expand)
+ Widget and select Table.Stale Pull Requests.Pull requests that have been open for more than 7 days.Save.... button in the top right corner of the table and select Customize table.
team_name mirror property).team_name, service and repository as the Group by columns.is_stale property and set the operator to = and the value to true.PR age distribution (click to expand)
+ Widget and select Pie chart.PR Age Distribution.PRs opened for: 0-3 days | 3-7 days | 7-30 days | >30 days.PR Age property.{
"combinator": "and",
"rules": [
{ "property": "status", "operator": "=", "value": "open" }
]
}{
"combinator": "and",
"rules": [
{ "property": "status", "operator": "=", "value": "opened" }
]
}{
"combinator": "and",
"rules": [
{ "property": "status", "operator": "=", "value": "active" }
]
}Save.
PR quality indicators
Track whether open PRs have proper reviewer and assignee coverage, which are key indicators of process health. Click Title: Description: Choose your PR/MR blueprint (GitHub Pull Request, Merge Request, or Pull Request). Add this JSON to the Initial filters editor: Click Click on the In the top right corner of the table, click on Manage Properties and add the following columns: Select Click on the save icon in the top right corner of the widget to save the customized table. Click Title: Description: Choose your PR/MR blueprint (GitHub Pull Request, Merge Request, or Pull Request). Under Breakdown by property, select the Add this JSON to the Additional filters editor: Click Azure DevOps pull requests do not have an assignee concept, so this widget applies only to GitHub and GitLab. Click Title: Description: Choose your PR/MR blueprint (GitHub Pull Request or Merge Request). Add this JSON to the Initial filters editor: Click Click on the In the top right corner of the table, click on Manage Properties and add the following columns: Select Click on the save icon in the top right corner of the widget to save the customized table. Azure DevOps pull requests do not have an assignee concept, so this widget applies only to GitHub and GitLab. Click Title: Description: Choose your PR/MR blueprint (GitHub Pull Request or Merge Request). Under Breakdown by property, select the Add this JSON to the Additional filters editor: Click Click Title: Description: Choose your PR/MR blueprint (GitHub Pull Request, Merge Request, or Pull Request). Add this JSON to the Initial filters editor: Click Click on the In the top right corner of the table, click on Manage Properties and add the following columns: Select Click on the save icon in the top right corner of the widget to save the customized table.PRs without assigned reviewers (click to expand)
+ Widget and select Table.PRs Without Assigned Reviewers.Lists pull requests that currently have no reviewers assigned.{
"combinator": "and",
"rules": [
{ "property": "status", "operator": "=", "value": "open" },
{ "property": "has_reviewers", "operator": "=", "value": false }
]
}{
"combinator": "and",
"rules": [
{ "property": "status", "operator": "=", "value": "opened" },
{ "property": "has_reviewers", "operator": "=", "value": false }
]
}{
"combinator": "and",
"rules": [
{ "property": "status", "operator": "=", "value": "active" },
{ "property": "has_reviewers", "operator": "=", "value": false }
]
}Save.... button in the top right corner of the table and select Customize table.
team_name, service and repository as the Group by columns.Open PR reviewer coverage (click to expand)
+ Widget and select Pie chart.Open PR Reviewer Coverage.Shows Open PRs with and without assigned reviewers.has_reviewers property.{
"combinator": "and",
"rules": [
{ "property": "status", "operator": "=", "value": "open" }
]
}{
"combinator": "and",
"rules": [
{ "property": "status", "operator": "=", "value": "opened" }
]
}{
"combinator": "and",
"rules": [
{ "property": "status", "operator": "=", "value": "active" }
]
}Save.PRs without assignees (GitHub / GitLab only) (click to expand)
+ Widget and select Table.PRs Without Assignees.Lists pull requests that currently have no assignees assigned.{
"combinator": "and",
"rules": [
{ "property": "status", "operator": "=", "value": "open" },
{ "property": "has_assignees", "operator": "=", "value": false }
]
}{
"combinator": "and",
"rules": [
{ "property": "status", "operator": "=", "value": "opened" },
{ "property": "has_assignees", "operator": "=", "value": false }
]
}Save.... button in the top right corner of the table and select Customize table.
team_name, service and repository as the Group by columns.Open PR assignment coverage (GitHub / GitLab only) (click to expand)
+ Widget and select Pie chart.Open PR Assignment Coverage.Shows open PRs with and without assigned owners.has_assignees property.{
"combinator": "and",
"rules": [
{ "property": "status", "operator": "=", "value": "open" }
]
}{
"combinator": "and",
"rules": [
{ "property": "status", "operator": "=", "value": "opened" }
]
}Save.All open pull requests (click to expand)
+ Widget and select Table.All Open Pull Requests.All currently open pull requests across repositories.{
"combinator": "and",
"rules": [
{ "property": "status", "operator": "=", "value": "open" }
]
}{
"combinator": "and",
"rules": [
{ "property": "status", "operator": "=", "value": "opened" }
]
}{
"combinator": "and",
"rules": [
{ "property": "status", "operator": "=", "value": "active" }
]
}Save.... button in the top right corner of the table and select Customize table.
days_old): How many days the PR has been open.has_assignees): Whether the PR has assignees (GitHub and GitLab only).has_reviewers): Whether the PR has reviewers.is_stale): Whether the PR has been open for more than 7 days.team_name, service and repository as the Group by columns.
Next steps
Once your Delivery Performance dashboard is in place, consider these additional improvements:
- Set up scorecards to automatically evaluate services and teams against delivery performance targets.
- Create automations to send Slack notifications when stale PR share exceeds a threshold or when PRs have been open without reviewers for more than 48 hours.
- Add an AI agent to provide natural language insights into your delivery data. See the delivery performance guide for configuration details.