Skip to content

Commit 900e3e9

Browse files
authored
Create PercentileMetrics.js
1 parent f4edc41 commit 900e3e9

File tree

1 file changed

+82
-0
lines changed
  • Core ServiceNow APIs/GlideAggregate/Incident resolution percentile by assignment group

1 file changed

+82
-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+
};

0 commit comments

Comments
 (0)