PAYATHON 2026

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提供状态背景,无法单独识别列变化
ColumnNameAnalytics 中的看板列名称判断每日快照中的工作项是否位于 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 接口

实施时按以下步骤处理:

  1. 先检查一个已知多次进出 Blocked 的工作项,确认修订记录包含目标看板对应的列值。
  2. 保留工作项 ID、修订号、System.ChangedDate 和已核实的列字段。
  3. 按修订号排序,将每条修订的生效时间与下一条修订的生效时间配成区间。
  4. 计算列值为 Blocked 的区间时长,再按工作项累加。
  5. 如果最后一条记录仍为 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。需要准确累计每次阻塞区间时,应先验证修订历史是否可用,再进行区间计算。前者使用微软文档支持的看板快照数据源,后者的时长计算需要自行实现和核对。

备注:内容仅供参考。