diff --git a/Core ServiceNow APIs/GlideAggregate/Incident resolution percentile by assignment group/PercentileMetrics.js b/Core ServiceNow APIs/GlideAggregate/Incident resolution percentile by assignment group/PercentileMetrics.js new file mode 100644 index 0000000000..2aab7892a0 --- /dev/null +++ b/Core ServiceNow APIs/GlideAggregate/Incident resolution percentile by assignment group/PercentileMetrics.js @@ -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} e.g. [0.5, 0.9] + * - table {String} table name (default 'incident') + * @returns {Array} [{ group: , 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' +}; diff --git a/Core ServiceNow APIs/GlideAggregate/Incident resolution percentile by assignment group/README.md b/Core ServiceNow APIs/GlideAggregate/Incident resolution percentile by assignment group/README.md new file mode 100644 index 0000000000..140d0371e0 --- /dev/null +++ b/Core ServiceNow APIs/GlideAggregate/Incident resolution percentile by assignment group/README.md @@ -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 diff --git a/Core ServiceNow APIs/GlideAggregate/Incident resolution percentile by assignment group/example_background_usage.js b/Core ServiceNow APIs/GlideAggregate/Incident resolution percentile by assignment group/example_background_usage.js new file mode 100644 index 0000000000..a39ae0118d --- /dev/null +++ b/Core ServiceNow APIs/GlideAggregate/Incident resolution percentile by assignment group/example_background_usage.js @@ -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'); + }); +})();