> For the complete documentation index, see llms.txt.
Skip to main content

Check out Port for yourself ➜ 

Track DORA metrics

Included by default

If you signed up for Port on or after May 1, 2026, DORA metrics are already tracked in your portal. Follow this guide to customize the DORA calculations to match your organization's deployment workflow and tooling.

The four DORA (DevOps Research and Assessment) metrics deployment frequency, lead time for changes, change failure rate, and mean time to recovery are the industry standard for measuring software delivery performance.
By tracking these metrics, you can identify areas for improvement and ensure your team is delivering high-quality software efficiently.

This guide walks you through building DORA metrics tracking in Port for deployment frequency, lead time for changes, change failure rate (CFR), and mean time to recovery (MTTR) at both the service and team level.

By the end of this guide, you will have a DORA metrics dashboard providing visibility into delivery performance across services and teams, with automatic tier classification (Elite, High, Medium, Low) based on DORA benchmarks.

Steps to build DORA metrics

  1. Validate your data model: ensure the foundational blueprints (service, Team) and relations are in place and aligned. DORA metrics build on top of this foundation.
  2. Track deployments: create the deployment blueprint and map merged PRs to deployment entities. This is the recommended default strategy. For organizations that require a more customized approach, alternative strategies such as workflow runs, CI/CD pipelines, or releases are also supported.
  3. Track incidents (optional): connect PagerDuty (or another tool) to enable CFR and MTTR.
  4. Configure metrics: add aggregation and tier calculation properties for deployment frequency, lead time, CFR, and MTTR at the service and team level.
  5. Build the dashboard: create widgets to visualize DORA metrics across your organization.

Prerequisites

This guide assumes the following:

  • A Port account with the onboarding process completed.
  • Foundational data model in place: complete the Create foundational Engineering Intelligence data model guide, which provisions the core Organization, Team, and Service blueprints (with team ownership on services) for GitHub, GitLab, and Azure DevOps.
  • A connected Git repository (GitHub, GitLab, or Azure Repos) linked to Port. Note: Other Git providers are supported, though this guide focuses on the three mentioned above.
  • An active Git integration: For GitHub users ensure Port's GitHub integration or Port's GitHub Ocean integration is installed.
  • (Optional) Incident Management integration: To track change failure rate and MTTR metrics, a PagerDuty integration with the pagerdutyIncident blueprint is required.
Incident-dependent metrics

Change failure rate and MTTR require an incident management integration with incident entities linked to services. This guide uses PagerDuty as the example, but the same approach applies to other tools like OpsGenie, FireHydrant, or ServiceNow. You just need to adjust the blueprint identifier and property names to match your integration.
Without an incident integration, only deployment frequency and lead time metrics will be available.

Tracking deployments

Deployments refer to releasing new or updated code into various environments such as Production, Staging, or Testing.
Tracking deployments helps you understand how efficiently your team ships features and monitor release stability.

By default, this guide creates deployment entities from merged pull requests to the default branch, which is the simplest and most common approach. If your organization requires a more customized deployment tracking strategy (e.g., via CI/CD pipelines, workflow runs, or releases), see Alternative deployment tracking strategies below.

Deployments contribute to three key DORA metrics: deployment frequency, change failure rate, and lead time for changes.

Create the deployment blueprint

  1. Navigate to your Port Builder page.

  2. Click the + Blueprint button to create a new blueprint.

  3. Click on the {...} Edit JSON button in the top right corner.

  4. Paste the JSON for your Git provider and click Save:

    Deployment blueprint (click to expand)
    {
    "identifier": "deployment",
    "title": "Deployment",
    "icon": "Deployment",
    "description": "A production deployment created from a merged PR to the default branch",
    "schema": {
    "properties": {
    "deploymentStatus": {
    "title": "Deployment Status",
    "type": "string",
    "enum": ["Success", "Failure"],
    "enumColors": {
    "Success": "green",
    "Failure": "red"
    }
    },
    "environment": {
    "title": "Environment",
    "type": "string",
    "enum": ["Production", "Staging", "Development"],
    "enumColors": {
    "Production": "green",
    "Staging": "yellow",
    "Development": "blue"
    }
    },
    "createdAt": {
    "title": "Deployment Time",
    "type": "string",
    "format": "date-time"
    }
    },
    "required": []
    },
    "mirrorProperties": {
    "github_lead_time_hours": {
    "title": "Lead Time for Changes (Hours)",
    "path": "github_pull_request.cycle_time_hours"
    },
    "github_repo_id": {
    "title": "GitHub Repository ID",
    "path": "github_pull_request.repository.$identifier"
    },
    "gitlab_repo_id": {
    "title": "GitLab Repository ID",
    "path": "gitlab_merge_request.repository.$identifier"
    },
    "ado_repo_id": {
    "title": "ADO Repository ID",
    "path": "ado_pull_request.repository.$identifier"
    }
    },
    "calculationProperties": {},
    "aggregationProperties": {},
    "relations": {
    "service": {
    "target": "service",
    "title": "Service",
    "many": false,
    "required": false
    },
    "github_pull_request": {
    "target": "githubPullRequest",
    "title": "Pull Request",
    "many": false,
    "required": false
    }
    }
    }

What you should see: After saving, the Deployment blueprint appears in your Builder with relations to both Service and your PR/MR blueprint, plus a lead time mirror property (github_lead_time_hours for GitHub, gitlab_lead_time_hours for GitLab, ado_lead_time_hours for Azure DevOps).

Missing lead time

If you do not have the lead time (cycle_time_hours) configured on your pull request / merge request blueprint, follow the relevant guide for your Git provider:

Alternatively, you can use the default leadTimeHours property that comes with some integrations and update the mirror property path to github_pull_request.leadTimeHours (GitHub), gitlab_merge_request.leadTimeHours (GitLab), or ado_pull_request.leadTimeHours (Azure DevOps).

Map deployments from merged PRs

The recommended approach is to create deployment entities automatically when pull requests are merged into the default branch. Each merged PR creates a deployment entity with the lead time calculated as the time from PR creation to merge. If your organization requires a more complex setup (e.g., workflow runs, CI/CD pipelines, releases, or custom API), see Alternative deployment tracking strategies below.

  1. Navigate to the data sources page in your Port portal.
  2. Select your Git integration.

Add the deployment mapping configuration for your provider:

Deployment mapping from merged PRs (click to expand)
Hardcoded values

The deploymentStatus is hardcoded to Success and environment to Production in these examples. You can modify these values based on your requirements.

- kind: pull-request
selector:
query: .base.ref == "main" and .state == "closed" and .merged_at != null
states: ["closed"]
maxResults: 100
since: 90
port:
entity:
mappings:
identifier: .head.repo.name + "-deploy-" + (.number | tostring)
title: '"Merge: " + .title'
blueprint: '"deployment"'
properties:
environment: '"Production"'
deploymentStatus: '"Success"'
createdAt: .merged_at
relations:
service: .head.repo.name
github_pull_request: .head.repo.name + "-pr-" + (.number | tostring)
Match your pull request mapping

Set github_pull_request to the same expression as the identifier in your GitHub Ocean pull-request resource (often .head.repo.name + "-pr-" + (.number|tostring)). Tune maxResults and since to limit how many closed pull requests sync. See GitHub Ocean examples and Migrate from the GitHub app.

Set up GitHub deployment service relations

The service relation on deployment entities is set in two ways:

  1. At ingest: the mapping sets service directly using .head.repo.name, matching the service identifier by repository name.
  2. Via automation: the set_github_deployment_relations automation fires whenever a service's github_repository relation changes and bulk-updates all deployment entities where github_pull_request is not null and github_repo_id matches the new repository identifier.
Prerequisite: github_repository relation on service

This automation requires the service blueprint to have a github_repository relation pointing to githubRepository. This relation is set up by the Create foundational Engineering Intelligence data model guide.

  1. Go to the automations page.
  2. Click + Automation.
  3. Click the {...} Edit JSON button.
  4. Paste the following JSON and click Save:
Set GitHub Deployment relations automation (click to expand)
{
"identifier": "set_github_deployment_relations",
"title": "Set GitHub Deployment relations",
"trigger": {
"type": "automation",
"event": {"type": "ANY_ENTITY_CHANGE", "blueprintIdentifier": "service"},
"condition": {
"type": "JQ",
"expressions": [
".diff.before.relations.github_repository != .diff.after.relations.github_repository",
".diff.after.relations.github_repository != null"
],
"combinator": "and"
}
},
"invocationMethod": {
"type": "WEBHOOK",
"url": "https://api.port.io/v1/migrations",
"agent": false,
"synchronized": true,
"method": "POST",
"headers": {"Content-Type": "application/json", "RUN_ID": "{{ .run.id }}"},
"body": {
"sourceBlueprint": "deployment",
"mapping": {
"blueprint": "deployment",
"filter": ".relations.github_pull_request != null and .properties.github_repo_id == \"{{ .event.diff.after.relations.github_repository }}\"",
"entity": {
"identifier": ".identifier",
"relations": {"service": "\"{{ .event.context.entityIdentifier }}\""}
}
}
}
}
}
Default branch

The mappings above filter for PRs merged to the main branch. If your repositories use a different default branch (e.g., master), update the filter accordingly.

  1. Click Save & Resync.

After resync, navigate to the Deployments catalog page in your portal. You should see deployment entities appearing for each merged PR, linked to the corresponding service and pull request.

Alternative deployment tracking strategies

If PR/MR merges don't fit your workflow, Port supports several other deployment tracking methods.

Other deployment tracking strategies (click to expand)

Workflow/Job runs

Track deployments by monitoring workflow runs in your pipeline. The deployment status is set dynamically based on whether the workflow concluded successfully or failed.

- kind: workflow-run
selector:
query: >
(.head_branch == "main") and
(.name | test("deploy|CD"; "i"))
port:
entity:
mappings:
identifier: .head_repository.name + "-deploy-" + (.run_number | tostring)
title: .head_repository.name + " Deployment via workflow"
blueprint: '"deployment"'
properties:
environment: '"Production"'
createdAt: .created_at
deploymentStatus: (.conclusion | ascii_upcase[0:1] + .[1:])
relations:
service: .head_repository.name

CI/CD pipelines (Jenkins, CircleCI, Azure Pipelines, etc.)

CI/CD pipelines can report deployments to Port using Port's API as part of the pipeline execution. See the relevant guide for your CI/CD tool:

These integrations use search relations to map the deployment to the correct service based on the service's $title. See mapping relations using search queries for more details.

Releases/Tags (GitHub only)

- kind: release
selector:
query: (.target_commitish == "main") and (.name | test("Production"; "i"))
port:
entity:
mappings:
identifier: .release.name + "-" + .release.tag_name
title: .release.name + " Deployment on release"
blueprint: '"deployment"'
properties:
environment: '"Production"'
createdAt: .release.created_at
deploymentStatus: '"Success"'
relations:
service: .repo.name

Find more details about setting up GitHub integrations for releases and tags in Repositories, repository releases and tags.

Custom API

If your tool or workflow is not natively supported, you can create deployment entities directly via Port's API:

curl -X POST https://api.port.io/v1/blueprints/deployment/entities?upsert=true&merge=true \
-H "Authorization: Bearer $YOUR_PORT_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"identifier": "custom-deployment-1234",
"title": "Custom Deployment 1234",
"properties": {
"environment": "Production",
"createdAt": "2024-09-01T12:00:00Z",
"deploymentStatus": "Success"
},
"relations": {
"service": "your-service-identifier"
}
}'

Replace $YOUR_PORT_API_TOKEN with your actual API token. See mapping relations using search queries for details.

Tracking incidents

Incidents are essential for tracking change failure rate (CFR) and mean time to recovery (MTTR). Effective incident tracking reveals how frequently deployments fail and how quickly teams resolve issues.

The steps below use PagerDuty as the example incident integration. If you use a different tool (OpsGenie, FireHydrant, ServiceNow, etc.), adapt the blueprint identifier (e.g., replace pagerdutyIncident with your incident blueprint) and property names accordingly.

Set up data model

Ensure that your PagerDuty incident blueprint is properly configured to map incidents to the correct services. Use the PagerDuty incident blueprint in the integration examples, and the default mapping configuration on the main PagerDuty page, as references when aligning your data model.

Add incident resolution time and recovery time properties:

  1. Navigate to your Port Builder page.

  2. Select the PagerDuty Incident blueprint.

  3. Click on the {...} button in the top right corner, and choose Edit JSON.

  4. Add the following properties:

    Additional PagerDuty incident properties (click to expand)
    "resolvedAt": {
    "title": "Incident Resolution Time",
    "type": "string",
    "format": "date-time",
    "description": "The timestamp when the incident was resolved"
    },
    "recoveryTime": {
    "title": "Time to Recovery",
    "type": "number",
    "description": "The time (in minutes) between the incident being triggered and resolved"
    }
  5. Click Save.

Add incident mapping config:

  1. Navigate to your Port Data Sources page.

  2. Select the PagerDuty data source.

  3. Add the following property mappings to the incident mapping section:

    Incident mapping for resolvedAt and recoveryTime (click to expand)
    resolvedAt: .resolved_at
    recoveryTime: >-
    (.created_at as $createdAt | .resolved_at as $resolvedAt |
    if $resolvedAt == null then null else
    ( ($resolvedAt | strptime("%Y-%m-%dT%H:%M:%SZ") | mktime) -
    ($createdAt | strptime("%Y-%m-%dT%H:%M:%SZ") | mktime) ) / 60 end)
  4. Click Save & Resync.

Syncing incidents

To sync incidents from PagerDuty, follow the PagerDuty guide. For other tools:

Automatic relations

The relation between the PagerDuty incident blueprint and the service blueprint is automatically created when you install the PagerDuty integration.

After resync, navigate to the PagerDuty Incidents catalog page in your portal. You should see incident entities appearing with resolvedAt and recoveryTime properties populated for resolved incidents, and each incident linked to the corresponding service.

Set up metrics

Now we'll add aggregation and calculation properties to compute DORA metrics and classify services and teams into performance tiers.

For each metric below, you will add properties to both the Service and Team blueprints. To edit a blueprint's JSON:

Relation chain for team-level metrics

Team-level aggregation properties scope data to each team's services. For Azure DevOps, azureDevopsPullRequest and azureDevopsBuild entities have inherited team ownership through their service relation, so no explicit pathFilter is needed. For GitHub PR aggregations, use pathFilter: [{fromBlueprint: "githubPullRequest", path: ["service", "_team"]}]. For GitLab MR aggregations, use pathFilter: [{fromBlueprint: "gitlabMergeRequest", path: ["service", "_team"]}]. Deployment aggregations work without a pathFilter for all providers.

  1. Go to the Builder in your Port portal.

  2. Click on the blueprint you want to edit (Service or Team).

  3. Click on the {...} button in the top right corner, and choose Edit JSON.

  4. Add the properties shown below to the "aggregationProperties" and "calculationProperties" sections.

  5. Click Save.

Aggregation data availability

Aggregation properties are calculated based on data ingested after the property is created. Historical data that was ingested before you add these properties will not be included. To backfill, trigger a resync on the relevant integration after saving the aggregation properties. See aggregation properties for more details.

Deployment frequency

Deployment frequency measures how often your services deploy to production. It is calculated as the average number of successful production deployments per week. The tier calculation classifies services as Elite (≥7/week), High (≥1/week), Medium (≥0.25/week), or Low.

Service level

Add the following to the Service blueprint:

Aggregation properties (click to expand)

Add to "aggregationProperties":

"total_deployments": {
"title": "Total Deployments",
"type": "number",
"target": "deployment",
"description": "Total successful deployments to Production",
"query": {
"combinator": "and",
"rules": [
{ "property": "deploymentStatus", "operator": "=", "value": "Success" },
{ "property": "environment", "operator": "=", "value": "Production" }
]
},
"calculationSpec": { "func": "count", "calculationBy": "entities" },
"pathFilter": [{ "fromBlueprint": "deployment", "path": ["service"] }]
},
"deployment_frequency": {
"title": "Deployment Frequency (per week)",
"type": "number",
"target": "deployment",
"description": "Average successful Production deployments per week",
"query": {
"combinator": "and",
"rules": [
{ "property": "deploymentStatus", "operator": "=", "value": "Success" },
{ "property": "environment", "operator": "=", "value": "Production" }
]
},
"calculationSpec": {
"func": "average",
"averageOf": "week",
"calculationBy": "entities",
"measureTimeBy": "createdAt"
},
"pathFilter": [{ "fromBlueprint": "deployment", "path": ["service"] }]
}
Tier calculation property (click to expand)

Add to "calculationProperties":

"deploy_freq_tier": {
"title": "Deployment Frequency",
"description": "DORA deployment frequency tier",
"type": "string",
"colorized": true,
"colors": {
"Low": "red",
"Medium": "orange",
"High": "blue",
"Elite": "lime"
},
"calculation": "if (.properties.total_deployments == null or .properties.total_deployments == 0) then \"Low\" else if (.properties.deployment_frequency // 0) >= 7 then \"Elite\" elif (.properties.deployment_frequency // 0) >= 1 then \"High\" elif (.properties.deployment_frequency // 0) >= 0.25 then \"Medium\" else \"Low\" end end"
}

Team level

Add the following to the Team blueprint. The team-level deployment frequency tier divides total deployment frequency by the number of services owned by the team, ensuring a fair cross-team comparison.

Select your Git provider. For deployment aggregations, all three providers use Port's native team ownership - no pathFilter is required:

Aggregation properties (click to expand)

Add to "aggregationProperties":

"services_count": {
"title": "Services Count",
"type": "number",
"target": "service",
"calculationSpec": { "func": "count", "calculationBy": "entities" }
},
"total_deployments": {
"title": "Total Deployments",
"type": "number",
"target": "deployment",
"description": "Total successful deployments across team services",
"query": {
"combinator": "and",
"rules": [
{ "property": "deploymentStatus", "operator": "=", "value": "Success" },
{ "property": "environment", "operator": "=", "value": "Production" }
]
},
"calculationSpec": { "func": "count", "calculationBy": "entities" }
},
"deployment_frequency": {
"title": "Deployment Frequency (per week)",
"type": "number",
"target": "deployment",
"description": "Average weekly deployments across team services",
"query": {
"combinator": "and",
"rules": [
{ "property": "deploymentStatus", "operator": "=", "value": "Success" },
{ "property": "environment", "operator": "=", "value": "Production" }
]
},
"calculationSpec": {
"func": "average",
"averageOf": "week",
"calculationBy": "entities",
"measureTimeBy": "createdAt"
}
}
GitHub native team ownership

For the _team blueprint, no pathFilter is required. Port's native team ownership automatically scopes aggregations to entities owned by each team, traversing through the service.$team ownership chain to reach deployments.

Tier calculation properties (click to expand)

Add to "calculationProperties":

"deployment_frequency_per_service": {
"title": "Deployment Frequency (per service)",
"description": "Deployment frequency normalized by the number of services",
"type": "number",
"calculation": "if (.properties.services_count != null and .properties.services_count != 0) then ((.properties.deployment_frequency // 0) / .properties.services_count) else 0 end"
},
"deploy_freq_tier": {
"title": "Deployment Frequency",
"description": "DORA deployment frequency tier (per service)",
"type": "string",
"colorized": true,
"colors": {
"Low": "red",
"Medium": "orange",
"High": "blue",
"Elite": "lime"
},
"calculation": "if (.properties.total_deployments == null or .properties.total_deployments == 0) then \"Low\" else (if (.properties.services_count != null and .properties.services_count != 0) then ((.properties.deployment_frequency // 0) / .properties.services_count) else 0 end) as $dpf | if $dpf >= 7 then \"Elite\" elif $dpf >= 1 then \"High\" elif $dpf >= 0.25 then \"Medium\" else \"Low\" end end"
}

What you should see: After saving both blueprints, open any service or team entity. You should see Total Deployments, Deployment Frequency (per week), and a Deployment Frequency tier badge (Elite/High/Medium/Low). Team entities also show Services Count and Deployment Frequency (per service).

Lead time for changes

Lead time for changes measures how quickly code moves from development into production. Port supports two approaches, you can choose the one that best fits your workflow:

Lead time is measured from when a pull request is created to when it is merged, using the cycle_time_hours property that Port calculates automatically as part of your PR/MR mapping. No additional setup is required - the aggregation below reads this property directly.

Scoping lead time to the default branch

Aggregation queries filter on blueprint properties in Port, not raw API field names. By default, the aggregation below includes merged PRs/MRs in the last 30 days. To scope by target branch, add a string property to your pull request blueprint (for example targetBranch) and map it from your Git provider (for example GitHub .base.ref, GitLab .target_branch, or Azure DevOps .targetRefName with refs/heads/ stripped). Then add a query rule such as "property": "targetBranch", "operator": "=", "value": "main".

The tier calculation classifies services as Elite (≤24h), High (≤168h / 1 week), Medium (≤720h / 30 days), or Low.

Using the first-commit method in the aggregation below

If you chose the first commit to merge method, replace "property": "cycle_time_hours" with "property": "first_commit_to_merge_hours" in each aggregation property below.

Service level

Add the following to the Service blueprint. Select your Git provider to get the correct target blueprint:

Aggregation property (click to expand)

Add to "aggregationProperties":

"github_lead_time_for_change": {
"title": "Lead Time for Changes (Hours)",
"type": "number",
"target": "githubPullRequest",
"description": "Average time from PR creation to merge in the last 30 days",
"query": {
"combinator": "and",
"rules": [
{ "property": "mergedAt", "operator": "between", "value": { "preset": "lastMonth" } }
]
},
"calculationSpec": {
"func": "average",
"averageOf": "total",
"calculationBy": "property",
"property": "cycle_time_hours",
"measureTimeBy": "$createdAt"
}
}
Tier calculation property (click to expand)

Add to "calculationProperties":

"github_lead_time_tier": {
"title": "Lead Time for Changes",
"description": "DORA lead time for changes tier",
"type": "string",
"colorized": true,
"colors": {
"Low": "red",
"Medium": "orange",
"High": "blue",
"Elite": "lime"
},
"calculation": "if (.properties.github_lead_time_for_change == null) then \"Low\" elif .properties.github_lead_time_for_change <= 24 then \"Elite\" elif .properties.github_lead_time_for_change <= 168 then \"High\" elif .properties.github_lead_time_for_change <= 720 then \"Medium\" else \"Low\" end"
}

Team level

Add the following to the Team blueprint. Select your Git provider - both the target and fromBlueprint in pathFilter must match:

Aggregation property (click to expand)

Add to "aggregationProperties":

"github_lead_time_for_change": {
"title": "Lead Time for Changes (Hours)",
"type": "number",
"target": "githubPullRequest",
"description": "Average lead time across team services in the last 30 days",
"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"] }]
}
Tier calculation property (click to expand)

Add to "calculationProperties":

"github_lead_time_tier": {
"title": "Lead Time for Changes",
"description": "DORA lead time for changes tier",
"type": "string",
"colorized": true,
"colors": {
"Low": "red",
"Medium": "orange",
"High": "blue",
"Elite": "lime"
},
"calculation": "if (.properties.github_lead_time_for_change == null) then \"Low\" elif .properties.github_lead_time_for_change <= 24 then \"Elite\" elif .properties.github_lead_time_for_change <= 168 then \"High\" elif .properties.github_lead_time_for_change <= 720 then \"Medium\" else \"Low\" end"
}

What you should see: After saving both blueprints, service and team entities should display Lead Time for Changes (Hours) and a Lead Time for Changes tier badge.

Change failure rate (CFR)

Requires incident integration

Change failure rate requires an incident management integration with incident entities linked to services. The examples below use PagerDuty (pagerdutyIncident), you can adjust the blueprint identifier and property names if you use a different tool. See the Tracking incidents section above.

Change failure rate measures the percentage of deployments that are associated with incidents. It is calculated as incidents / (deployments + incidents) × 100. The tier calculation classifies services as Elite (≤5%), High (≤20%), Medium (≤30%), or Low.

CFR calculation approach

The standard DORA definition of CFR is failed deployments / total deployments. Since most incident tools don't directly link incidents to specific deployments, this guide uses incident count as a proxy for failed deployments. This is a common and practical adaptation and if your workflow allows you to mark deployments as failed directly (e.g., via a deploymentStatus of Failed), you can adjust the formula accordingly.

Service level

Add the following to the Service blueprint:

Aggregation property (click to expand)

Add to "aggregationProperties":

"total_incidents": {
"title": "Total Incidents",
"type": "number",
"target": "pagerdutyIncident",
"description": "Total incidents linked to this service",
"calculationSpec": {
"func": "count",
"calculationBy": "entities"
},
"pathFilter": [
{
"fromBlueprint": "pagerdutyIncident",
"path": ["service"]
}
]
}
Tier calculation property (click to expand)

Add to "calculationProperties":

"change_failure_rate": {
"title": "Change Failure Rate (%)",
"description": "Percentage of deployments that caused incidents",
"type": "number",
"calculation": "if (.properties.total_deployments == null or .properties.total_deployments == 0) then null else ((.properties.total_incidents // 0) / (.properties.total_deployments + (.properties.total_incidents // 0)) * 100 | floor) end"
},
"cfr_tier": {
"title": "CFR",
"description": "DORA change failure rate tier",
"type": "string",
"colorized": true,
"colors": {
"Low": "red",
"Medium": "orange",
"High": "blue",
"Elite": "lime"
},
"calculation": "if (.properties.change_failure_rate == null) then null else (if .properties.change_failure_rate <= 5 then \"Elite\" elif .properties.change_failure_rate <= 20 then \"High\" elif .properties.change_failure_rate <= 30 then \"Medium\" else \"Low\" end) end"
}

Team level

Add the following to the Team blueprint:

Aggregation property (click to expand)

Add to "aggregationProperties":

"total_incidents": {
"title": "Total Incidents",
"type": "number",
"target": "pagerdutyIncident",
"description": "Total incidents across team services",
"calculationSpec": {
"func": "count",
"calculationBy": "entities"
}
}
GitLab users

For GitLab, add a pathFilter to scope the aggregation to the explicit team relation: "pathFilter": [{"fromBlueprint": "pagerdutyIncident", "path": ["service", "team"]}]. GitHub and Azure DevOps use Port's native team ownership, so no pathFilter is needed.

Tier calculation properties (click to expand)

Add to "calculationProperties":

"change_failure_rate": {
"title": "Change Failure Rate (%)",
"description": "Percentage of deployments that caused incidents",
"type": "number",
"calculation": "if (.properties.total_deployments == null or .properties.total_deployments == 0) then null else ((.properties.total_incidents // 0) / (.properties.total_deployments + (.properties.total_incidents // 0)) * 100 | floor) end"
},
"cfr_tier": {
"title": "CFR",
"description": "DORA change failure rate tier",
"type": "string",
"colorized": true,
"colors": {
"Low": "red",
"Medium": "orange",
"High": "blue",
"Elite": "lime"
},
"calculation": "if (.properties.change_failure_rate == null) then null else (if .properties.change_failure_rate <= 5 then \"Elite\" elif .properties.change_failure_rate <= 20 then \"High\" elif .properties.change_failure_rate <= 30 then \"Medium\" else \"Low\" end) end"
}

What you should see: After saving both blueprints, service entities show Total Incidents and a CFR tier badge. Team entities additionally show a Change Failure Rate (%) value.

Mean time to recovery (MTTR)

MTTR measures the average time from incident trigger to resolution, reflecting how quickly teams recover from failures. The tier calculation classifies services as Elite (≤60min), High (≤1,440min / 1 day), Medium (≤43,200min / 30 days), or Low.

Requires incident integration

MTTR requires an incident management integration with incident entities that include a recovery time property. The examples below use PagerDuty (pagerdutyIncident with recoveryTime), you can adjust the blueprint identifier and property names if you use a different tool. See the Tracking incidents section above.

Service level

Add the following to the Service blueprint:

Aggregation property (click to expand)

Add to "aggregationProperties":

"mean_time_to_recovery": {
"title": "MTTR (Minutes)",
"type": "number",
"target": "pagerdutyIncident",
"description": "Average time in minutes from incident trigger to resolution",
"calculationSpec": {
"func": "average",
"averageOf": "total",
"calculationBy": "property",
"property": "recoveryTime",
"measureTimeBy": "$createdAt"
},
"pathFilter": [
{
"fromBlueprint": "pagerdutyIncident",
"path": ["service"]
}
]
}
Tier calculation property (click to expand)

Add to "calculationProperties":

"mttr_tier": {
"title": "MTTR",
"description": "DORA MTTR tier",
"type": "string",
"colorized": true,
"colors": {
"Low": "red",
"Medium": "orange",
"High": "blue",
"Elite": "lime"
},
"calculation": "if (.properties.total_incidents == null or .properties.total_incidents == 0) then null elif (.properties.mean_time_to_recovery == null) then \"Elite\" elif .properties.mean_time_to_recovery <= 60 then \"Elite\" elif .properties.mean_time_to_recovery <= 1440 then \"High\" elif .properties.mean_time_to_recovery <= 43200 then \"Medium\" else \"Low\" end"
}

Team level

Add the following to the Team blueprint:

Aggregation property (click to expand)

Add to "aggregationProperties":

"mean_time_to_recovery": {
"title": "MTTR (Minutes)",
"type": "number",
"target": "pagerdutyIncident",
"description": "Average recovery time across team services",
"calculationSpec": {
"func": "average",
"averageOf": "total",
"calculationBy": "property",
"property": "recoveryTime",
"measureTimeBy": "$createdAt"
}
}
GitLab users

For GitLab, add a pathFilter to scope the aggregation to the explicit team relation: "pathFilter": [{"fromBlueprint": "pagerdutyIncident", "path": ["service", "team"]}]. GitHub and Azure DevOps use Port's native team ownership, so no pathFilter is needed.

Tier calculation property (click to expand)

Add to "calculationProperties":

"mttr_tier": {
"title": "MTTR",
"description": "DORA MTTR tier",
"type": "string",
"colorized": true,
"colors": {
"Low": "red",
"Medium": "orange",
"High": "blue",
"Elite": "lime"
},
"calculation": "if (.properties.total_incidents == null or .properties.total_incidents == 0) then null elif (.properties.mean_time_to_recovery == null) then \"Elite\" elif .properties.mean_time_to_recovery <= 60 then \"Elite\" elif .properties.mean_time_to_recovery <= 1440 then \"High\" elif .properties.mean_time_to_recovery <= 43200 then \"Medium\" else \"Low\" end"
}

What you should see: After saving both blueprints, service and team entities show MTTR (Minutes) and an MTTR tier badge.

Visualize metrics

We will create a dedicated dashboard to monitor DORA metrics using Port's customizable widgets. The dashboard covers deployment frequency, lead time, and optionally change failure rate and MTTR.

Create the dashboard

  1. Navigate to your software catalog.
  2. Click on the + button in the left sidebar.
  3. Select New folder (if you don't already have one).
  4. Name the folder Engineering Intelligence and click Create. The folder identifier will be automatically set to engineering_intelligence - this is required for the API script method to work.
  5. Inside the Engineering Intelligence folder, click + New again.
  6. Select New dashboard.
  7. Name the dashboard DORA Metrics and click Create.

Add widgets

You can populate the dashboard using either an API script or by manually creating each widget through the UI.

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

  1. In your Port portal, click on your profile picture in the top right corner.

  2. Select Credentials.

  3. Click Generate API token.

  4. Copy the generated token and store it as an environment variable:

    export PORT_ACCESS_TOKEN="YOUR_GENERATED_TOKEN"
EU region

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

Provider-specific property names

The dashboard JSON below includes columns for both GitHub and GitLab in the service and team tables. The KPI and trend widgets use GitHub-specific property names - if you are using GitLab, replace github_lead_time_hours with gitlab_lead_time_for_change in those widgets. If you are using Azure DevOps, replace github_lead_time_hours with ado_lead_time_hours, github_lead_time_tier with ado_lead_time_tier, and github_lead_time_for_change with ado_lead_time_for_change in the KPI/trend widgets and table columns.

Save the following JSON to a file named dora_dashboard.json:

Dashboard JSON payload (click to expand)
{
"identifier": "dora_metrics",
"title": "DORA Metrics",
"icon": "Health",
"type": "dashboard",
"parent": "engineering_intelligence",
"widgets": [
{
"id": "doraDashboardWidget",
"type": "dashboard-widget",
"layout": [
{
"height": 400,
"columns": [
{"id": "deployFreqKpi", "size": 6},
{"id": "deployFreqChart", "size": 6}
]
},
{
"height": 520,
"columns": [
{"id": "teamDoraTable", "size": 12}
]
},
{
"height": 520,
"columns": [
{"id": "groupDoraTable", "size": 12}
]
},
{
"height": 520,
"columns": [
{"id": "serviceDoraTable", "size": 12}
]
},
{
"height": 400,
"columns": [
{"id": "doraGithubLeadTimeKpi", "size": 6},
{"id": "doraGithubLeadTimeChart", "size": 6}
]
},
{
"height": 400,
"columns": [
{"id": "cfrKpi", "size": 6},
{"id": "cfrChart", "size": 6}
]
},
{
"height": 400,
"columns": [
{"id": "mttrKpi", "size": 6},
{"id": "mttrChart", "size": 6}
]
}
],
"widgets": [
{
"id": "deployFreqKpi",
"type": "entities-number-chart",
"title": "Avg Deployment Frequency",
"icon": "Metric",
"description": "Average weekly deployments per service across all teams across organization",
"blueprint": "_team",
"chartType": "aggregateByProperty",
"calculationBy": "property",
"func": "average",
"property": "deployment_frequency_per_service",
"averageOf": "total",
"displayFormatting": "custom",
"decimalPlaces": ".00",
"unit": "custom",
"unitCustom": "per week",
"dataset": {
"combinator": "and",
"rules": [
{"operator": "=", "property": "type", "value": "team"}
]
}
},
{
"id": "deployFreqChart",
"type": "line-chart",
"title": "Number of Deployments per Day",
"icon": "LineChart",
"description": "Number of deployments per day across the organization",
"blueprint": "deployment",
"chartType": "countEntities",
"func": "count",
"measureTimeBy": "createdAt",
"timeInterval": "day",
"timeRange": {"preset": "lastMonth"},
"xAxisTitle": "Day",
"yAxisTitle": "Deployments",
"dataset": {
"combinator": "and",
"rules": [
{"operator": "=", "property": "deploymentStatus", "value": "Success"},
{"operator": "=", "property": "environment", "value": "Production"}
]
}
},
{
"id": "teamDoraTable",
"type": "table-entities-explorer",
"displayMode": "widget",
"title": "Team - DORA Metrics",
"icon": "Table",
"description": "Scored against DORA benchmarks: Elite / High / Medium / Low",
"blueprint": "_team",
"dataset": {"combinator": "and", "rules": [{"operator": "=", "property": "type", "value": "team"}]},
"excludedFields": [],
"blueprintConfig": {
"_team": {
"groupSettings": {"groupBy": []},
"propertiesSettings": {
"order": ["$title", "deploy_freq_tier", "deployment_frequency_per_service", "github_lead_time_tier", "gitlab_lead_time_tier", "ado_lead_time_tier", "github_lead_time_for_change", "gitlab_lead_time_for_change", "ado_lead_time_for_change", "cfr_tier", "change_failure_rate", "mttr_tier", "mean_time_to_recovery", "total_incidents"],
"shown": ["$title", "deploy_freq_tier", "deployment_frequency_per_service", "github_lead_time_tier", "gitlab_lead_time_tier", "ado_lead_time_tier", "github_lead_time_for_change", "gitlab_lead_time_for_change", "ado_lead_time_for_change", "cfr_tier", "change_failure_rate", "mttr_tier", "mean_time_to_recovery", "total_incidents"]
},
"filterSettings": {"filterBy": {"combinator": "and", "rules": [{"operator": "=", "property": "type", "value": "team"}]}},
"sortSettings": {"sortBy": [{"property": "deployment_frequency_per_service", "order": "desc"}]}
}
}
},
{
"id": "groupDoraTable",
"type": "table-entities-explorer",
"displayMode": "widget",
"title": "Group - DORA Metrics",
"icon": "Table",
"description": "Scored against DORA benchmarks: Elite / High / Medium / Low",
"blueprint": "_team",
"dataset": {"combinator": "and", "rules": [{"operator": "=", "property": "type", "value": "group"}]},
"excludedFields": [],
"blueprintConfig": {
"_team": {
"groupSettings": {"groupBy": []},
"propertiesSettings": {
"order": ["$title", "deploy_freq_tier", "deployment_frequency_per_service", "github_lead_time_tier", "gitlab_lead_time_tier", "ado_lead_time_tier", "github_lead_time_for_change", "gitlab_lead_time_for_change", "ado_lead_time_for_change", "cfr_tier", "change_failure_rate", "mttr_tier", "mean_time_to_recovery", "total_incidents"],
"shown": ["$title", "deploy_freq_tier", "deployment_frequency_per_service", "github_lead_time_tier", "gitlab_lead_time_tier", "ado_lead_time_tier", "github_lead_time_for_change", "gitlab_lead_time_for_change", "ado_lead_time_for_change", "cfr_tier", "change_failure_rate", "mttr_tier", "mean_time_to_recovery", "total_incidents"]
},
"filterSettings": {"filterBy": {"combinator": "and", "rules": [{"operator": "=", "property": "type", "value": "group"}]}},
"sortSettings": {"sortBy": [{"property": "deployment_frequency_per_service", "order": "desc"}]}
}
}
},
{
"id": "serviceDoraTable",
"type": "table-entities-explorer",
"displayMode": "widget",
"title": "Service - DORA Metrics",
"icon": "Table",
"description": "Scored against DORA benchmarks: Elite / High / Medium / Low",
"blueprint": "service",
"dataset": {"combinator": "and", "rules": []},
"excludedFields": [],
"blueprintConfig": {
"service": {
"groupSettings": {"groupBy": ["team"]},
"propertiesSettings": {
"order": ["$title", "team", "deploy_freq_tier", "deployment_frequency", "github_lead_time_tier", "gitlab_lead_time_tier", "ado_lead_time_tier", "github_lead_time_for_change", "gitlab_lead_time_for_change", "ado_lead_time_for_change", "cfr_tier", "change_failure_rate", "mttr_tier", "mean_time_to_recovery", "total_incidents"],
"shown": ["$title", "team", "deploy_freq_tier", "deployment_frequency", "github_lead_time_tier", "gitlab_lead_time_tier", "ado_lead_time_tier", "github_lead_time_for_change", "gitlab_lead_time_for_change", "ado_lead_time_for_change", "cfr_tier", "change_failure_rate", "mttr_tier", "mean_time_to_recovery", "total_incidents"]
},
"filterSettings": {"filterBy": {"combinator": "and", "rules": []}},
"sortSettings": {"sortBy": [{"property": "deployment_frequency", "order": "desc"}]}
}
}
},
{
"id": "doraGithubLeadTimeKpi",
"type": "entities-number-chart",
"title": "Avg Lead Time for Changes (GitHub)",
"icon": "Metric",
"description": "Average lead time from linked GitHub PRs for production deployments in the last 30 days",
"blueprint": "deployment",
"chartType": "aggregateByProperty",
"calculationBy": "property",
"func": "average",
"property": "github_lead_time_hours",
"averageOf": "total",
"measureTimeBy": "createdAt",
"displayFormatting": "custom",
"decimalPlaces": ".00",
"unit": "custom",
"unitCustom": "Hours",
"dataset": {
"combinator": "and",
"rules": [
{"operator": "=", "property": "deploymentStatus", "value": "Success"},
{"operator": "=", "property": "environment", "value": "Production"},
{"operator": "between", "property": "createdAt", "value": {"preset": "lastMonth"}}
]
}
},
{
"id": "doraGithubLeadTimeChart",
"type": "line-chart",
"title": "Daily Mean Lead Time for Changes (GitHub)",
"icon": "LineChart",
"description": "Average lead time per day from linked GitHub pull requests for production deployments",
"blueprint": "deployment",
"chartType": "aggregatePropertiesValues",
"func": "average",
"properties": ["properties.github_lead_time_hours"],
"measureTimeBy": "createdAt",
"timeInterval": "day",
"timeRange": {"preset": "lastMonth"},
"xAxisTitle": "Day",
"yAxisTitle": "Hours",
"dataset": {
"combinator": "and",
"rules": [
{"operator": "=", "property": "deploymentStatus", "value": "Success"},
{"operator": "=", "property": "environment", "value": "Production"}
]
}
},
{
"id": "cfrKpi",
"type": "entities-number-chart",
"title": "Avg Change Failure Rate (CFR)",
"icon": "Metric",
"description": "Average change failure rate across all teams in the organization",
"blueprint": "_team",
"chartType": "aggregateByProperty",
"calculationBy": "property",
"func": "average",
"property": "change_failure_rate",
"averageOf": "total",
"displayFormatting": "round",
"unit": "custom",
"unitCustom": "%",
"dataset": {
"combinator": "and",
"rules": [
{"operator": "=", "property": "type", "value": "team"}
]
}
},
{
"id": "cfrChart",
"type": "line-chart",
"title": "Daily Incidents Count",
"icon": "LineChart",
"description": "Number of incidents per day across the organization",
"blueprint": "pagerdutyIncident",
"chartType": "countEntities",
"func": "count",
"measureTimeBy": "created_at",
"timeInterval": "day",
"timeRange": {"preset": "lastMonth"},
"xAxisTitle": "Day",
"yAxisTitle": "Incidents",
"dataset": {"combinator": "and", "rules": []}
},
{
"id": "mttrKpi",
"type": "entities-number-chart",
"title": "Avg Daily Mean Time to Recovery (MTTR)",
"icon": "Metric",
"description": "Average mean time to recovery across the organization in the last 30 days",
"blueprint": "pagerdutyIncident",
"chartType": "aggregateByProperty",
"calculationBy": "property",
"func": "average",
"property": "recoveryTime",
"averageOf": "total",
"displayFormatting": "round",
"unit": "custom",
"unitCustom": "Minutes",
"dataset": {
"combinator": "and",
"rules": [
{"operator": "between", "property": "resolvedAt", "value": {"preset": "lastMonth"}}
]
}
},
{
"id": "mttrChart",
"type": "line-chart",
"title": "Daily Mean Time to Recovery (MTTR)",
"icon": "LineChart",
"description": "Average incident resolution time per day across the organization",
"blueprint": "pagerdutyIncident",
"chartType": "aggregatePropertiesValues",
"func": "average",
"properties": ["properties.recoveryTime"],
"measureTimeBy": "resolvedAt",
"timeInterval": "day",
"timeRange": {"preset": "lastMonth"},
"xAxisTitle": "Day",
"yAxisTitle": "Minutes",
"dataset": {"combinator": "and", "rules": []}
}
]
}
]
}

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 @dora_dashboard.json | python3 -m json.tool
Engineering Intelligence folder

The script assumes an engineering_intelligence folder already exists in your catalog. If you haven't created it yet, follow steps 1-4 in the create the dashboard section first.

Next steps

Once your DORA metrics dashboard is in place, consider these additional improvements:

  • Set up DORA scorecards to automatically evaluate services and teams against DORA performance targets and track improvement over time.
  • Add incident integration (PagerDuty) to unlock change failure rate and MTTR metrics for the full four-metric DORA picture.
  • Create automations to send Slack notifications when a service's DORA tier drops below a threshold or when deployment frequency declines significantly.
  • Add an AI agent to provide natural language insights into your DORA data directly on the dashboard.