Azure DevOps 如何统计工作项在 Blocked Kanban 列的累计停留时间?
结论
可以统计。即使 State 一直保持为 Active,也可以根据工作项进入和离开 Blocked 列的历史计算阻塞时长。
具体方案取决于精度要求:
- 按每日快照估算天数:在 Power BI 中通过 Analytics OData 查询
WorkItemBoardSnapshot,统计每个工作项被快照记录在Blocked列的日期数。 - 按实际经过时间计算,允许小数天:读取能够反映目标看板列变化的工作项修订历史,累加各次停留区间。
标准 Analytics Views 无法直接提供这个列历史报表,因为 Board Column、Board Column Done 和 Board Lane 不能作为视图中的筛选或选择字段。微软文档给出的替代入口是通过 OData 使用 WorkItemBoardSnapshot。Analytics Views 字段限制
为什么不能只根据 State 或 Blocked 标记计算?
在这个场景中,工作项可以多次进入和离开 Blocked 列,但 State 一直是 Active。按 State 分组统计时间,无法区分正常处理时间和阻塞时间。
还需要区分三个概念:
| 数据 | 含义 | 在当前需求中的用途 |
|---|---|---|
State | 工作流状态,例如 Active | 提供状态背景,无法单独识别列变化 |
ColumnName | Analytics 中的看板列名称 | 判断每日快照中的工作项是否位于 Blocked 列 |
Blocked 字段 | 工作项中的阻塞属性 | 只有其历史值与列位置保持同步时,才能作为替代依据 |
例如,Analytics 文档列出的 CMMI Blocked 字段对应 Microsoft.VSTS.CMMI.Blocked,值为 Yes 或 No。列名也叫 Blocked,并不意味着两者会自动同步。Azure Boards Analytics 字段参考
Azure DevOps 内置功能能直接算吗?
普通工作项查询可以查找当前位于某列或曾经进入某列的工作项,但筛选条件本身不会累加每次进入和离开之间的时长。Changed Date 记录的是工作项发生修改的时间,也不能直接当作进入 Blocked 列的时间。看板列查询说明
内置的 Cumulative Flow Diagram(CFD)适合观察各列工作项数量随时间的变化,但它不直接提供每个工作项多次进入 Blocked 列后的累计时长。需要另行建立报表计算。
方案一:通过 Power BI 统计每日快照
使用哪个数据源?
使用 Analytics OData 的 WorkItemBoardSnapshot,无需寻找预设的 Analytics View。微软的 CFD 示例也使用这个实体集读取看板历史。官方 OData 示例
建议至少读取:
| 字段或条件 | 用途 |
|---|---|
WorkItemId | 按工作项汇总 |
DateValue | 标识快照日期 |
ColumnName | 判断是否为 Blocked |
Team/TeamName | 限定目标团队 |
BoardName | 限定目标看板 |
State | 可选,用于核对工作流状态 |
必须先限定团队和看板,避免把同一个工作项在不同看板上的记录合并。
获取数据
在 Power BI Desktop 中选择“获取数据 → 空白查询”,打开高级编辑器。以下示例沿用官方示例的 V3.0-preview 路径。执行前,替换组织、项目、团队、看板名称及起始日期,并确认服务支持该端点:
let
Source = OData.Feed(
"https://analytics.dev.azure.com/{organization}/{project}/_odata/V3.0-preview/WorkItemBoardSnapshot?"
& "$select=WorkItemId,DateValue,ColumnName,BoardName,State"
& "&$filter=Team/TeamName eq '{teamname}'"
& " and BoardName eq '{boardname}'"
& " and DateValue ge 2026-01-01Z",
null,
[Implementation="2.0", ODataVersion=4]
)
in
Source
示例日期只是报表起点。名称中的特殊字符需要按 OData 和 URL 规则转义。如果只查询最近一段时间,结果也只代表这段时间内的快照天数。
创建度量值
将查询表命名为 BoardSnapshots,创建以下 DAX 度量值:
Blocked Snapshot Days =
COALESCE(
CALCULATE(
DISTINCTCOUNT('BoardSnapshots'[DateValue]),
'BoardSnapshots'[ColumnName] = "Blocked"
),
0
)
在表格可视化中放入 WorkItemId 和该度量值。如果数据已限定到一个团队和看板,每行会显示该工作项位于 Blocked 列的不同快照日期数,多次进入该列的日期会一起累计。
这个度量值用于逐工作项展示。总计会在总计筛选上下文中重新计算日期去重数,并非所有工作项天数之和。需要求总计时,应另行按 WorkItemId 求和。
每日快照的精度限制
WorkItemBoardSnapshot 是每日快照,不是完整的列移动事件流。如果工作项上午进入、下午离开 Blocked,当天的阻塞过程可能没有被快照捕获。
因此,上面的结果应命名为“Blocked 快照天数”,不能宣称它等于实际停留小时数除以 24。官方文档说明,快照按固定周期记录数据,无法保留周期内的所有变化。Analytics 历史数据模型
方案二:按修订历史累计实际停留时间
如果必须统计同一天内多次进出,或者需要得到 1.5 天这样的结果,应使用带时间戳的变更历史。
可以通过 Work Item Tracking REST API 获取工作项的完整修订记录。以下是 API 7.1 的请求形式,实际使用时应选择部署支持的版本,并处理分页:
GET https://dev.azure.com/{organization}/{project}/_apis/wit/workItems/{id}/revisions?api-version=7.1
该接口返回修订号和各修订中的字段值,可据此整理时间序列。Revisions - List 接口
实施时按以下步骤处理:
- 先检查一个已知多次进出
Blocked的工作项,确认修订记录包含目标看板对应的列值。 - 保留工作项 ID、修订号、
System.ChangedDate和已核实的列字段。 - 按修订号排序,将每条修订的生效时间与下一条修订的生效时间配成区间。
- 计算列值为
Blocked的区间时长,再按工作项累加。 - 如果最后一条记录仍为
Blocked,则计算到明确的报表截止时间。
计算逻辑可以表示为:
blocked_seconds = 0
for each revision in chronological_order:
start = revision.changed_at
end = next_revision.changed_at or report_cutoff
if revision.column == "Blocked":
interval_start = max(start, report_start)
interval_end = min(end, report_cutoff)
blocked_seconds += max(0, interval_end - interval_start)
blocked_days = blocked_seconds / 86400
这里的列值必须已经确认能代表目标看板位置。不要先删除非 Blocked 修订再计算相邻时间差,否则会丢失离开该列的时间,把中间正常处理的时间也算进去。
实施时需要确认的边界
- 跨团队看板:微软提醒,同一个工作项出现在多个团队看板时,其他团队的操作可能影响
Board Column等字段。因此,不能未经核对就将System.BoardColumn的历史视为某个指定看板的完整历史。多看板字段注意事项 - 统计窗口:按修订计算时,要读取窗口开始前最后一条有效记录,才能判断工作项在窗口开始时是否已经处于
Blocked。 - 天数定义:秒数除以
86400得到连续经过时间对应的天数,包含周末。若要统计工作日或工作时段,需要另外应用工作日历。 - 历史缺失:如果现有修订没有保留可用的目标列信息,就无法仅凭当前列值还原过去的精确时长。可以从后续开始记录列移动事件,或维护与阻塞状态同步的自定义字段,但不能据此补造旧数据。
日级趋势报表可优先采用 WorkItemBoardSnapshot。需要准确累计每次阻塞区间时,应先验证修订历史是否可用,再进行区间计算。前者使用微软文档支持的看板快照数据源,后者的时长计算需要自行实现和核对。
备注:内容仅供参考。
