Skip to content

Commit a1fd5d8

Browse files
authored
GlideAggregate - Incident resolution percentile (#2384)
* Create README.md * Create PercentileMetrics.js * Create example_background_usage.js
1 parent 3aa97b0 commit a1fd5d8

File tree

3 files changed

+122
-0
lines changed

3 files changed

+122
-0
lines changed
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// Script Include: PercentileMetrics
2+
// Purpose: Compute percentile resolution times by group using nearest-rank selection.
3+
// Scope: global or scoped. Client callable false.
4+
5+
var PercentileMetrics = Class.create();
6+
PercentileMetrics.prototype = {
7+
initialize: function() {},
8+
9+
/**
10+
* Compute percentiles for incident resolution times by group.
11+
* @param {Object} options
12+
* - windowDays {Number} lookback window (default 30)
13+
* - groupField {String} field to group by (default 'assignment_group')
14+
* - percentiles {Array<Number>} e.g. [0.5, 0.9]
15+
* - table {String} table name (default 'incident')
16+
* @returns {Array<Object>} [{ group: <sys_id>, count: N, avgMins: X, p: { '0.5': v, '0.9': v } }]
17+
*/
18+
resolutionPercentiles: function(options) {
19+
var opts = options || {};
20+
var table = opts.table || 'incident';
21+
var groupField = opts.groupField || 'assignment_group';
22+
var windowDays = Number(opts.windowDays || 30);
23+
var pct = Array.isArray(opts.percentiles) && opts.percentiles.length ? opts.percentiles : [0.5, 0.9];
24+
25+
// Build date cutoff for resolved incidents
26+
var cutoff = new GlideDateTime();
27+
cutoff.addDaysUTC(-windowDays);
28+
29+
// First pass: find candidate groups with counts and avg
30+
var ga = new GlideAggregate(table);
31+
ga.addQuery('resolved_at', '>=', cutoff);
32+
ga.addQuery('state', '>=', 6); // resolved/closed states
33+
ga.addAggregate('COUNT');
34+
ga.addAggregate('AVG', 'calendar_duration'); // average of resolution duration
35+
ga.groupBy(groupField);
36+
ga.query();
37+
38+
var results = [];
39+
while (ga.next()) {
40+
var groupId = ga.getValue(groupField);
41+
var count = parseInt(ga.getAggregate('COUNT'), 10) || 0;
42+
if (!groupId || count === 0) continue;
43+
44+
// Second pass: ordered sample to pick percentile ranks
45+
var ordered = new GlideRecord(table);
46+
ordered.addQuery('resolved_at', '>=', cutoff);
47+
ordered.addQuery('state', '>=', '6');
48+
ordered.addQuery(groupField, groupId);
49+
ordered.addNotNullQuery('closed_at');
50+
// Approx resolution minutes using dateDiff: closed_at - opened_at in minutes
51+
ordered.addQuery('opened_at', 'ISNOTEMPTY');
52+
ordered.addQuery('closed_at', 'ISNOTEMPTY');
53+
ordered.orderBy('closed_at'); // for stability
54+
ordered.query();
55+
56+
var durations = [];
57+
while (ordered.next()) {
58+
var opened = String(ordered.getValue('opened_at'));
59+
var closed = String(ordered.getValue('closed_at'));
60+
var mins = gs.dateDiff(opened, closed, true) / 60; // seconds -> minutes
61+
durations.push(mins);
62+
}
63+
durations.sort(function(a, b) { return a - b; });
64+
65+
var pvals = {};
66+
pct.forEach(function(p) {
67+
var rank = Math.max(1, Math.ceil(p * durations.length)); // nearest-rank
68+
pvals[String(p)] = durations.length ? Math.round(durations[rank - 1]) : 0;
69+
});
70+
71+
results.push({
72+
group: groupId,
73+
count: count,
74+
avgMins: Math.round(parseFloat(ga.getAggregate('AVG', 'calendar_duration')) / 60),
75+
p: pvals
76+
});
77+
}
78+
return results;
79+
},
80+
81+
type: 'PercentileMetrics'
82+
};
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Incident resolution percentile by assignment group
2+
3+
## What this solves
4+
Leaders often ask for P50 or P90 of incident resolution time by assignment group. Out-of-box reports provide averages, but percentiles are more meaningful for skewed distributions. This utility computes configurable percentiles from incident resolution durations.
5+
6+
## Where to use
7+
- Script Include callable from Background Scripts, Scheduled Jobs, or Flow Actions
8+
- Example Background Script is included
9+
10+
## How it works
11+
- Uses `GlideAggregate` to get candidate groups with resolved incidents in a time window
12+
- For each group, queries resolved incidents ordered by resolution duration (ascending)
13+
- Picks percentile ranks (for example 0.5, 0.9) using nearest-rank method
14+
- Returns a simple object per group with count, average minutes, and requested percentiles
15+
16+
## Configure
17+
- `WINDOW_DAYS`: number of days to look back (default 30)
18+
- `GROUP_FIELD`: field to group by (default `assignment_group`)
19+
- Percentiles array (for example `[0.5, 0.9]`)
20+
21+
## References
22+
- GlideAggregate API
23+
https://www.servicenow.com/docs/bundle/zurich-api-reference/page/app-store/dev_portal/API_reference/GlideAggregate/concept/c_GlideAggregateAPI.html
24+
- GlideRecord API
25+
https://www.servicenow.com/docs/bundle/zurich-api-reference/page/app-store/dev_portal/API_reference/GlideRecord/concept/c_GlideRecordAPI.html
26+
- GlideDateTime API
27+
https://www.servicenow.com/docs/bundle/zurich-api-reference/page/app-store/dev_portal/API_reference/GlideDateTime/concept/c_GlideDateTimeAPI.html
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Background Script: example usage for PercentileMetrics
2+
(function() {
3+
var util = new PercentileMetrics();
4+
var out = util.resolutionPercentiles({
5+
windowDays: 30,
6+
groupField: 'assignment_group',
7+
percentiles: [0.5, 0.9, 0.95]
8+
});
9+
10+
out.forEach(function(r) {
11+
gs.info('Group=' + r.group + ' count=' + r.count + ' avg=' + r.avgMins + 'm P50=' + r.p['0.5'] + 'm P90=' + r.p['0.9'] + 'm P95=' + r.p['0.95'] + 'm');
12+
});
13+
})();

0 commit comments

Comments
 (0)