Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Script Include: PercentileMetrics
// Purpose: Compute percentile resolution times by group using nearest-rank selection.
// Scope: global or scoped. Client callable false.

var PercentileMetrics = Class.create();
PercentileMetrics.prototype = {
initialize: function() {},

/**
* Compute percentiles for incident resolution times by group.
* @param {Object} options
* - windowDays {Number} lookback window (default 30)
* - groupField {String} field to group by (default 'assignment_group')
* - percentiles {Array<Number>} e.g. [0.5, 0.9]
* - table {String} table name (default 'incident')
* @returns {Array<Object>} [{ group: <sys_id>, count: N, avgMins: X, p: { '0.5': v, '0.9': v } }]
*/
resolutionPercentiles: function(options) {
var opts = options || {};
var table = opts.table || 'incident';
var groupField = opts.groupField || 'assignment_group';
var windowDays = Number(opts.windowDays || 30);
var pct = Array.isArray(opts.percentiles) && opts.percentiles.length ? opts.percentiles : [0.5, 0.9];

// Build date cutoff for resolved incidents
var cutoff = new GlideDateTime();
cutoff.addDaysUTC(-windowDays);

// First pass: find candidate groups with counts and avg
var ga = new GlideAggregate(table);
ga.addQuery('resolved_at', '>=', cutoff);
ga.addQuery('state', '>=', 6); // resolved/closed states
ga.addAggregate('COUNT');
ga.addAggregate('AVG', 'calendar_duration'); // average of resolution duration
ga.groupBy(groupField);
ga.query();

var results = [];
while (ga.next()) {
var groupId = ga.getValue(groupField);
var count = parseInt(ga.getAggregate('COUNT'), 10) || 0;
if (!groupId || count === 0) continue;

// Second pass: ordered sample to pick percentile ranks
var ordered = new GlideRecord(table);
ordered.addQuery('resolved_at', '>=', cutoff);
ordered.addQuery('state', '>=', '6');
ordered.addQuery(groupField, groupId);
ordered.addNotNullQuery('closed_at');
// Approx resolution minutes using dateDiff: closed_at - opened_at in minutes
ordered.addQuery('opened_at', 'ISNOTEMPTY');
ordered.addQuery('closed_at', 'ISNOTEMPTY');
ordered.orderBy('closed_at'); // for stability
ordered.query();

var durations = [];
while (ordered.next()) {
var opened = String(ordered.getValue('opened_at'));
var closed = String(ordered.getValue('closed_at'));
var mins = gs.dateDiff(opened, closed, true) / 60; // seconds -> minutes
durations.push(mins);
}
durations.sort(function(a, b) { return a - b; });

var pvals = {};
pct.forEach(function(p) {
var rank = Math.max(1, Math.ceil(p * durations.length)); // nearest-rank
pvals[String(p)] = durations.length ? Math.round(durations[rank - 1]) : 0;
});

results.push({
group: groupId,
count: count,
avgMins: Math.round(parseFloat(ga.getAggregate('AVG', 'calendar_duration')) / 60),
p: pvals
});
}
return results;
},

type: 'PercentileMetrics'
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Incident resolution percentile by assignment group

## What this solves
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.

## Where to use
- Script Include callable from Background Scripts, Scheduled Jobs, or Flow Actions
- Example Background Script is included

## How it works
- Uses `GlideAggregate` to get candidate groups with resolved incidents in a time window
- For each group, queries resolved incidents ordered by resolution duration (ascending)
- Picks percentile ranks (for example 0.5, 0.9) using nearest-rank method
- Returns a simple object per group with count, average minutes, and requested percentiles

## Configure
- `WINDOW_DAYS`: number of days to look back (default 30)
- `GROUP_FIELD`: field to group by (default `assignment_group`)
- Percentiles array (for example `[0.5, 0.9]`)

## References
- GlideAggregate API
https://www.servicenow.com/docs/bundle/zurich-api-reference/page/app-store/dev_portal/API_reference/GlideAggregate/concept/c_GlideAggregateAPI.html
- GlideRecord API
https://www.servicenow.com/docs/bundle/zurich-api-reference/page/app-store/dev_portal/API_reference/GlideRecord/concept/c_GlideRecordAPI.html
- GlideDateTime API
https://www.servicenow.com/docs/bundle/zurich-api-reference/page/app-store/dev_portal/API_reference/GlideDateTime/concept/c_GlideDateTimeAPI.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Background Script: example usage for PercentileMetrics
(function() {
var util = new PercentileMetrics();
var out = util.resolutionPercentiles({
windowDays: 30,
groupField: 'assignment_group',
percentiles: [0.5, 0.9, 0.95]
});

out.forEach(function(r) {
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');
});
})();
Loading