@pipeline-builder/pipeline-data 4.1.23 → 4.1.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/api/alert-destinations.d.ts +17 -0
- package/lib/api/alert-destinations.js +36 -0
- package/lib/api/reporting/analytics-queries.d.ts +274 -0
- package/lib/api/reporting/analytics-queries.js +598 -0
- package/lib/api/reporting/daily-rollup.d.ts +41 -0
- package/lib/api/reporting/daily-rollup.js +173 -0
- package/lib/api/reporting/deploy-rollup.d.ts +78 -0
- package/lib/api/reporting/deploy-rollup.js +203 -0
- package/lib/api/reporting/dora-scoring.d.ts +14 -0
- package/lib/api/reporting/dora-scoring.js +25 -1
- package/lib/api/reporting/dora.js +30 -6
- package/lib/api/reporting/failure-classifier.d.ts +54 -0
- package/lib/api/reporting/failure-classifier.js +179 -0
- package/lib/api/reporting/ingest.d.ts +2 -2
- package/lib/api/reporting/ingest.js +129 -4
- package/lib/api/reporting/last-deploy.d.ts +31 -0
- package/lib/api/reporting/last-deploy.js +93 -0
- package/lib/api/reporting/needs-attention.d.ts +93 -0
- package/lib/api/reporting/needs-attention.js +152 -0
- package/lib/api/reporting/plugin-exposure.d.ts +129 -0
- package/lib/api/reporting/plugin-exposure.js +312 -0
- package/lib/api/reporting/retention-sweep.js +9 -1
- package/lib/api/reporting/sql-helpers.d.ts +35 -2
- package/lib/api/reporting/sql-helpers.js +39 -1
- package/lib/api/reporting/stakeholder/compose.d.ts +115 -0
- package/lib/api/reporting/stakeholder/compose.js +234 -0
- package/lib/api/reporting/stakeholder/index.d.ts +14 -0
- package/lib/api/reporting/stakeholder/index.js +17 -0
- package/lib/api/reporting/stakeholder/period.d.ts +102 -0
- package/lib/api/reporting/stakeholder/period.js +273 -0
- package/lib/api/reporting/stakeholder/report-html.d.ts +93 -0
- package/lib/api/reporting/stakeholder/report-html.js +208 -0
- package/lib/api/reporting/stakeholder/sections.d.ts +117 -0
- package/lib/api/reporting/stakeholder/sections.js +249 -0
- package/lib/api/reporting/stakeholder/store.d.ts +450 -0
- package/lib/api/reporting/stakeholder/store.js +981 -0
- package/lib/api/reporting/stakeholder/templates.d.ts +21 -0
- package/lib/api/reporting/stakeholder/templates.js +104 -0
- package/lib/api/reporting/types.d.ts +73 -0
- package/lib/api/reporting/types.js +1 -1
- package/lib/api/reporting-service.d.ts +94 -5
- package/lib/api/reporting-service.js +169 -10
- package/lib/api/soft-delete-sweep.d.ts +11 -2
- package/lib/api/soft-delete-sweep.js +17 -6
- package/lib/database/drizzle-schema.d.ts +2263 -385
- package/lib/database/drizzle-schema.js +18 -7
- package/lib/database/index.d.ts +1 -1
- package/lib/database/index.js +4 -2
- package/lib/database/schema/compliance.d.ts +0 -682
- package/lib/database/schema/compliance.js +6 -66
- package/lib/database/schema/ecosystem.d.ts +20 -157
- package/lib/database/schema/ecosystem.js +23 -16
- package/lib/database/schema/pipeline.d.ts +200 -0
- package/lib/database/schema/pipeline.js +100 -1
- package/lib/database/schema/plugin.d.ts +20 -1
- package/lib/database/schema/plugin.js +13 -1
- package/lib/database/schema/reporting-analytics.d.ts +1212 -0
- package/lib/database/schema/reporting-analytics.js +250 -0
- package/lib/database/schema/reporting-stakeholder.d.ts +1405 -0
- package/lib/database/schema/reporting-stakeholder.js +251 -0
- package/lib/index.d.ts +10 -1
- package/lib/index.js +15 -1
- package/lib/testing/exports.json +67 -4
- package/package.json +4 -3
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How long after a day closes before it is rolled up (hours).
|
|
3
|
+
*
|
|
4
|
+
* Six by default, matching the report scheduler's own settle delay: both exist for
|
|
5
|
+
* the same reason, and a rollup that lagged the report would make a scheduled
|
|
6
|
+
* report read a half-built day.
|
|
7
|
+
*/
|
|
8
|
+
export declare function rollupSettleHours(): number;
|
|
9
|
+
/** Rows per rebuild pass. Bounds the statement, not the backlog. */
|
|
10
|
+
export declare function rollupBatchDays(): number;
|
|
11
|
+
/** The UTC midnight that starts `d`'s day. */
|
|
12
|
+
export declare function utcDayStart(d: Date): Date;
|
|
13
|
+
/**
|
|
14
|
+
* Rebuild the rollup rows for one UTC day, for every org that had activity.
|
|
15
|
+
*
|
|
16
|
+
* ONE statement, computed entirely in Postgres. Deliberately not "select the
|
|
17
|
+
* events, aggregate in Node, write the rows": the whole point of the rollup is to
|
|
18
|
+
* avoid moving a day's events across the wire, and a job that did it to BUILD the
|
|
19
|
+
* rollup would just move the cost rather than remove it.
|
|
20
|
+
*
|
|
21
|
+
* Percentiles use `PERCENTILE_CONT`, which is the same function the on-demand
|
|
22
|
+
* queries use — so a rollup-backed number and a live number agree rather than
|
|
23
|
+
* differing by an interpolation rule.
|
|
24
|
+
*/
|
|
25
|
+
export declare function rebuildDay(day: Date): Promise<number>;
|
|
26
|
+
/**
|
|
27
|
+
* One rollup pass: rebuild the most recent settled days.
|
|
28
|
+
*
|
|
29
|
+
* Rebuilding a WINDOW rather than only the newest day is what handles late
|
|
30
|
+
* events: the dead-letter redrive lands events for days already rolled up, and a
|
|
31
|
+
* job that only ever built "yesterday" would leave those days permanently wrong.
|
|
32
|
+
* Recomputing a week costs one statement per day and makes lateness self-healing
|
|
33
|
+
* instead of a support question.
|
|
34
|
+
*
|
|
35
|
+
* Never throws — the caller is a scheduler tick, and a failed pass must retry next
|
|
36
|
+
* tick rather than take the service's background work down with it.
|
|
37
|
+
*/
|
|
38
|
+
export declare function runRollupPass(now?: Date): Promise<{
|
|
39
|
+
days: number;
|
|
40
|
+
rows: number;
|
|
41
|
+
}>;
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// Copyright 2026 Pipeline Builder Contributors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* Building `execution_daily_rollups` from events.
|
|
5
|
+
*
|
|
6
|
+
* Every section that says "runs", "success rate", "p95" or "build minutes" reads
|
|
7
|
+
* these rows, so a quarterly report over a busy org costs a few hundred rows
|
|
8
|
+
* instead of a few hundred thousand — and keeps working after retention has swept
|
|
9
|
+
* the events it was built from.
|
|
10
|
+
*
|
|
11
|
+
* TWO PROPERTIES THE JOB HAS TO HAVE, and both shape the code:
|
|
12
|
+
*
|
|
13
|
+
* - IDEMPOTENT PER DAY. A day is recomputed WHOLESALE and upserted, never
|
|
14
|
+
* incremented. Incrementing is how a redelivered event, a retried tick or a
|
|
15
|
+
* crashed half-run silently inflates a count, and an inflated success rate is
|
|
16
|
+
* worse than a missing one because nobody can tell.
|
|
17
|
+
* - REBUILDS A DAY WHEN LATE EVENTS ARRIVE. The ingest's dead-letter queue is
|
|
18
|
+
* redriven hours later, so yesterday's numbers legitimately change. The
|
|
19
|
+
* `computed_at` stamp is what makes that visible rather than mysterious.
|
|
20
|
+
*
|
|
21
|
+
* THE SETTLE DELAY lives here too: a day is only rolled up once it has been closed
|
|
22
|
+
* long enough for in-flight executions to finish. Rolling up a day at 00:01 counts
|
|
23
|
+
* every execution still running at midnight as neither a success nor a failure.
|
|
24
|
+
*/
|
|
25
|
+
import { createLogger, errorMessage, envInt } from '@pipeline-builder/api-core';
|
|
26
|
+
import { sql } from 'drizzle-orm';
|
|
27
|
+
import { withTenantTx } from '../../database/tenancy.js';
|
|
28
|
+
const logger = createLogger('daily-rollup');
|
|
29
|
+
/**
|
|
30
|
+
* How long after a day closes before it is rolled up (hours).
|
|
31
|
+
*
|
|
32
|
+
* Six by default, matching the report scheduler's own settle delay: both exist for
|
|
33
|
+
* the same reason, and a rollup that lagged the report would make a scheduled
|
|
34
|
+
* report read a half-built day.
|
|
35
|
+
*/
|
|
36
|
+
export function rollupSettleHours() {
|
|
37
|
+
return envInt('REPORTING_ROLLUP_SETTLE_HOURS', 6, { min: 0 });
|
|
38
|
+
}
|
|
39
|
+
/** Rows per rebuild pass. Bounds the statement, not the backlog. */
|
|
40
|
+
export function rollupBatchDays() {
|
|
41
|
+
return envInt('REPORTING_ROLLUP_BATCH_DAYS', 7, { min: 1 });
|
|
42
|
+
}
|
|
43
|
+
/** The UTC midnight that starts `d`'s day. */
|
|
44
|
+
export function utcDayStart(d) {
|
|
45
|
+
return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()));
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Rebuild the rollup rows for one UTC day, for every org that had activity.
|
|
49
|
+
*
|
|
50
|
+
* ONE statement, computed entirely in Postgres. Deliberately not "select the
|
|
51
|
+
* events, aggregate in Node, write the rows": the whole point of the rollup is to
|
|
52
|
+
* avoid moving a day's events across the wire, and a job that did it to BUILD the
|
|
53
|
+
* rollup would just move the cost rather than remove it.
|
|
54
|
+
*
|
|
55
|
+
* Percentiles use `PERCENTILE_CONT`, which is the same function the on-demand
|
|
56
|
+
* queries use — so a rollup-backed number and a live number agree rather than
|
|
57
|
+
* differing by an interpolation rule.
|
|
58
|
+
*/
|
|
59
|
+
export async function rebuildDay(day) {
|
|
60
|
+
const dayStart = utcDayStart(day);
|
|
61
|
+
const dayEnd = new Date(dayStart.getTime() + 24 * 3600_000);
|
|
62
|
+
// TWO GRAINS, one scan, and no GROUPING SETS needed: a PIPELINE event carries
|
|
63
|
+
// no stage name, so it collapses to the `''` pipeline-level row, and a STAGE
|
|
64
|
+
// event lands on its own stage row. The event shape gives the hierarchy for
|
|
65
|
+
// free.
|
|
66
|
+
//
|
|
67
|
+
// CTEs rather than window functions, because `PERCENTILE_CONT` is an ordered-set
|
|
68
|
+
// aggregate and cannot be used as a window function at all — the shape that
|
|
69
|
+
// looks natural here does not run.
|
|
70
|
+
const result = await withTenantTx((tx) => tx.execute(sql `
|
|
71
|
+
WITH ev AS (
|
|
72
|
+
SELECT
|
|
73
|
+
org_id,
|
|
74
|
+
pipeline_id::text AS pipeline_id,
|
|
75
|
+
COALESCE(stage_name, '') AS stage_name,
|
|
76
|
+
status,
|
|
77
|
+
failure_category,
|
|
78
|
+
duration_ms
|
|
79
|
+
FROM pipeline_events
|
|
80
|
+
WHERE created_at >= ${dayStart} AND created_at < ${dayEnd}
|
|
81
|
+
AND pipeline_id IS NOT NULL
|
|
82
|
+
AND event_type IN ('PIPELINE', 'STAGE')
|
|
83
|
+
),
|
|
84
|
+
cats AS (
|
|
85
|
+
SELECT org_id, pipeline_id, stage_name,
|
|
86
|
+
jsonb_object_agg(failure_category, n) AS failures_by_category
|
|
87
|
+
FROM (
|
|
88
|
+
SELECT org_id, pipeline_id, stage_name, failure_category, COUNT(*)::int AS n
|
|
89
|
+
FROM ev
|
|
90
|
+
WHERE failure_category IS NOT NULL
|
|
91
|
+
GROUP BY 1, 2, 3, 4
|
|
92
|
+
) t
|
|
93
|
+
GROUP BY 1, 2, 3
|
|
94
|
+
),
|
|
95
|
+
agg AS (
|
|
96
|
+
SELECT
|
|
97
|
+
org_id, pipeline_id, stage_name,
|
|
98
|
+
COUNT(*)::int AS runs,
|
|
99
|
+
COUNT(*) FILTER (WHERE status IN ('SUCCEEDED', 'SUCCESS'))::int AS succeeded,
|
|
100
|
+
COUNT(*) FILTER (WHERE status IN ('FAILED', 'FAILURE', 'ERROR', 'TIMED_OUT'))::int AS failed,
|
|
101
|
+
-- NULL durations are ignored by PERCENTILE_CONT, which is what we want: an
|
|
102
|
+
-- event with no duration has no timing to contribute, and treating it as
|
|
103
|
+
-- zero would drag every percentile down.
|
|
104
|
+
PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY duration_ms)::int AS p50_ms,
|
|
105
|
+
PERCENTILE_CONT(0.9) WITHIN GROUP (ORDER BY duration_ms)::int AS p90_ms,
|
|
106
|
+
PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY duration_ms)::int AS p95_ms,
|
|
107
|
+
(SUM(COALESCE(duration_ms, 0)) / 1000)::int AS build_seconds
|
|
108
|
+
FROM ev
|
|
109
|
+
GROUP BY 1, 2, 3
|
|
110
|
+
)
|
|
111
|
+
INSERT INTO execution_daily_rollups (
|
|
112
|
+
org_id, day, pipeline_id, stage_name,
|
|
113
|
+
runs, succeeded, failed, failures_by_category,
|
|
114
|
+
p50_ms, p90_ms, p95_ms, build_seconds, computed_at
|
|
115
|
+
)
|
|
116
|
+
SELECT
|
|
117
|
+
a.org_id, ${dayStart}::timestamptz, a.pipeline_id, a.stage_name,
|
|
118
|
+
a.runs, a.succeeded, a.failed,
|
|
119
|
+
COALESCE(c.failures_by_category, '{}'::jsonb),
|
|
120
|
+
a.p50_ms, a.p90_ms, a.p95_ms, a.build_seconds, NOW()
|
|
121
|
+
FROM agg a
|
|
122
|
+
LEFT JOIN cats c
|
|
123
|
+
ON c.org_id = a.org_id AND c.pipeline_id = a.pipeline_id AND c.stage_name = a.stage_name
|
|
124
|
+
ON CONFLICT (org_id, day, pipeline_id, stage_name) DO UPDATE SET
|
|
125
|
+
runs = excluded.runs,
|
|
126
|
+
succeeded = excluded.succeeded,
|
|
127
|
+
failed = excluded.failed,
|
|
128
|
+
failures_by_category = excluded.failures_by_category,
|
|
129
|
+
p50_ms = excluded.p50_ms,
|
|
130
|
+
p90_ms = excluded.p90_ms,
|
|
131
|
+
p95_ms = excluded.p95_ms,
|
|
132
|
+
build_seconds = excluded.build_seconds,
|
|
133
|
+
computed_at = excluded.computed_at
|
|
134
|
+
`));
|
|
135
|
+
const rows = result.rowCount ?? 0;
|
|
136
|
+
logger.debug('Rolled up a day', { day: dayStart.toISOString().slice(0, 10), rows });
|
|
137
|
+
return rows;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* One rollup pass: rebuild the most recent settled days.
|
|
141
|
+
*
|
|
142
|
+
* Rebuilding a WINDOW rather than only the newest day is what handles late
|
|
143
|
+
* events: the dead-letter redrive lands events for days already rolled up, and a
|
|
144
|
+
* job that only ever built "yesterday" would leave those days permanently wrong.
|
|
145
|
+
* Recomputing a week costs one statement per day and makes lateness self-healing
|
|
146
|
+
* instead of a support question.
|
|
147
|
+
*
|
|
148
|
+
* Never throws — the caller is a scheduler tick, and a failed pass must retry next
|
|
149
|
+
* tick rather than take the service's background work down with it.
|
|
150
|
+
*/
|
|
151
|
+
export async function runRollupPass(now = new Date()) {
|
|
152
|
+
const settleMs = rollupSettleHours() * 3600_000;
|
|
153
|
+
// The newest day that has settled. A day is only built once everything that
|
|
154
|
+
// started in it has had time to finish.
|
|
155
|
+
const newest = utcDayStart(new Date(now.getTime() - settleMs));
|
|
156
|
+
let rows = 0;
|
|
157
|
+
let days = 0;
|
|
158
|
+
for (let i = 0; i < rollupBatchDays(); i++) {
|
|
159
|
+
const day = new Date(newest.getTime() - i * 24 * 3600_000);
|
|
160
|
+
try {
|
|
161
|
+
rows += await rebuildDay(day);
|
|
162
|
+
days++;
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
logger.warn('Rollup pass failed for a day; it will be retried next pass', {
|
|
166
|
+
day: day.toISOString().slice(0, 10),
|
|
167
|
+
error: errorMessage(err),
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return { days, rows };
|
|
172
|
+
}
|
|
173
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZGFpbHktcm9sbHVwLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vc3JjL2FwaS9yZXBvcnRpbmcvZGFpbHktcm9sbHVwLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLCtDQUErQztBQUMvQyxzQ0FBc0M7QUFFdEM7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7OztHQXFCRztBQUVILE9BQU8sRUFBRSxZQUFZLEVBQUUsWUFBWSxFQUFFLE1BQU0sRUFBRSxNQUFNLDRCQUE0QixDQUFDO0FBQ2hGLE9BQU8sRUFBRSxHQUFHLEVBQUUsTUFBTSxhQUFhLENBQUM7QUFDbEMsT0FBTyxFQUFFLFlBQVksRUFBRSxNQUFNLDJCQUEyQixDQUFDO0FBRXpELE1BQU0sTUFBTSxHQUFHLFlBQVksQ0FBQyxjQUFjLENBQUMsQ0FBQztBQUU1Qzs7Ozs7O0dBTUc7QUFDSCxNQUFNLFVBQVUsaUJBQWlCO0lBQy9CLE9BQU8sTUFBTSxDQUFDLCtCQUErQixFQUFFLENBQUMsRUFBRSxFQUFFLEdBQUcsRUFBRSxDQUFDLEVBQUUsQ0FBQyxDQUFDO0FBQ2hFLENBQUM7QUFFRCxvRUFBb0U7QUFDcEUsTUFBTSxVQUFVLGVBQWU7SUFDN0IsT0FBTyxNQUFNLENBQUMsNkJBQTZCLEVBQUUsQ0FBQyxFQUFFLEVBQUUsR0FBRyxFQUFFLENBQUMsRUFBRSxDQUFDLENBQUM7QUFDOUQsQ0FBQztBQUVELDhDQUE4QztBQUM5QyxNQUFNLFVBQVUsV0FBVyxDQUFDLENBQU87SUFDakMsT0FBTyxJQUFJLElBQUksQ0FBQyxJQUFJLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQyxjQUFjLEVBQUUsRUFBRSxDQUFDLENBQUMsV0FBVyxFQUFFLEVBQUUsQ0FBQyxDQUFDLFVBQVUsRUFBRSxDQUFDLENBQUMsQ0FBQztBQUNqRixDQUFDO0FBRUQ7Ozs7Ozs7Ozs7O0dBV0c7QUFDSCxNQUFNLENBQUMsS0FBSyxVQUFVLFVBQVUsQ0FBQyxHQUFTO0lBQ3hDLE1BQU0sUUFBUSxHQUFHLFdBQVcsQ0FBQyxHQUFHLENBQUMsQ0FBQztJQUNsQyxNQUFNLE1BQU0sR0FBRyxJQUFJLElBQUksQ0FBQyxRQUFRLENBQUMsT0FBTyxFQUFFLEdBQUcsRUFBRSxHQUFHLFFBQVEsQ0FBQyxDQUFDO0lBRTVELDhFQUE4RTtJQUM5RSw2RUFBNkU7SUFDN0UsNEVBQTRFO0lBQzVFLFFBQVE7SUFDUixFQUFFO0lBQ0YsaUZBQWlGO0lBQ2pGLDRFQUE0RTtJQUM1RSxtQ0FBbUM7SUFDbkMsTUFBTSxNQUFNLEdBQUcsTUFBTSxZQUFZLENBQUMsQ0FBQyxFQUFFLEVBQUUsRUFBRSxDQUFDLEVBQUUsQ0FBQyxPQUFPLENBQUMsR0FBRyxDQUFBOzs7Ozs7Ozs7OzRCQVU5QixRQUFRLHFCQUFxQixNQUFNOzs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7O2tCQXFDN0MsUUFBUTs7Ozs7Ozs7Ozs7Ozs7Ozs7R0FpQnZCLENBQUMsQ0FBQyxDQUFDO0lBQ0osTUFBTSxJQUFJLEdBQUksTUFBMkMsQ0FBQyxRQUFRLElBQUksQ0FBQyxDQUFDO0lBQ3hFLE1BQU0sQ0FBQyxLQUFLLENBQUMsaUJBQWlCLEVBQUUsRUFBRSxHQUFHLEVBQUUsUUFBUSxDQUFDLFdBQVcsRUFBRSxDQUFDLEtBQUssQ0FBQyxDQUFDLEVBQUUsRUFBRSxDQUFDLEVBQUUsSUFBSSxFQUFFLENBQUMsQ0FBQztJQUNwRixPQUFPLElBQUksQ0FBQztBQUNkLENBQUM7QUFFRDs7Ozs7Ozs7Ozs7R0FXRztBQUNILE1BQU0sQ0FBQyxLQUFLLFVBQVUsYUFBYSxDQUFDLEdBQUcsR0FBUyxJQUFJLElBQUksRUFBRTtJQUN4RCxNQUFNLFFBQVEsR0FBRyxpQkFBaUIsRUFBRSxHQUFHLFFBQVEsQ0FBQztJQUNoRCw0RUFBNEU7SUFDNUUsd0NBQXdDO0lBQ3hDLE1BQU0sTUFBTSxHQUFHLFdBQVcsQ0FBQyxJQUFJLElBQUksQ0FBQyxHQUFHLENBQUMsT0FBTyxFQUFFLEdBQUcsUUFBUSxDQUFDLENBQUMsQ0FBQztJQUMvRCxJQUFJLElBQUksR0FBRyxDQUFDLENBQUM7SUFDYixJQUFJLElBQUksR0FBRyxDQUFDLENBQUM7SUFDYixLQUFLLElBQUksQ0FBQyxHQUFHLENBQUMsRUFBRSxDQUFDLEdBQUcsZUFBZSxFQUFFLEVBQUUsQ0FBQyxFQUFFLEVBQUUsQ0FBQztRQUMzQyxNQUFNLEdBQUcsR0FBRyxJQUFJLElBQUksQ0FBQyxNQUFNLENBQUMsT0FBTyxFQUFFLEdBQUcsQ0FBQyxHQUFHLEVBQUUsR0FBRyxRQUFRLENBQUMsQ0FBQztRQUMzRCxJQUFJLENBQUM7WUFDSCxJQUFJLElBQUksTUFBTSxVQUFVLENBQUMsR0FBRyxDQUFDLENBQUM7WUFDOUIsSUFBSSxFQUFFLENBQUM7UUFDVCxDQUFDO1FBQUMsT0FBTyxHQUFHLEVBQUUsQ0FBQztZQUNiLE1BQU0sQ0FBQyxJQUFJLENBQUMsNERBQTRELEVBQUU7Z0JBQ3hFLEdBQUcsRUFBRSxHQUFHLENBQUMsV0FBVyxFQUFFLENBQUMsS0FBSyxDQUFDLENBQUMsRUFBRSxFQUFFLENBQUM7Z0JBQ25DLEtBQUssRUFBRSxZQUFZLENBQUMsR0FBRyxDQUFDO2FBQ3pCLENBQUMsQ0FBQztRQUNMLENBQUM7SUFDSCxDQUFDO0lBQ0QsT0FBTyxFQUFFLElBQUksRUFBRSxJQUFJLEVBQUUsQ0FBQztBQUN4QixDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiLy8gQ29weXJpZ2h0IDIwMjYgUGlwZWxpbmUgQnVpbGRlciBDb250cmlidXRvcnNcbi8vIFNQRFgtTGljZW5zZS1JZGVudGlmaWVyOiBBcGFjaGUtMi4wXG5cbi8qKlxuICogQnVpbGRpbmcgYGV4ZWN1dGlvbl9kYWlseV9yb2xsdXBzYCBmcm9tIGV2ZW50cy5cbiAqXG4gKiBFdmVyeSBzZWN0aW9uIHRoYXQgc2F5cyBcInJ1bnNcIiwgXCJzdWNjZXNzIHJhdGVcIiwgXCJwOTVcIiBvciBcImJ1aWxkIG1pbnV0ZXNcIiByZWFkc1xuICogdGhlc2Ugcm93cywgc28gYSBxdWFydGVybHkgcmVwb3J0IG92ZXIgYSBidXN5IG9yZyBjb3N0cyBhIGZldyBodW5kcmVkIHJvd3NcbiAqIGluc3RlYWQgb2YgYSBmZXcgaHVuZHJlZCB0aG91c2FuZCDigJQgYW5kIGtlZXBzIHdvcmtpbmcgYWZ0ZXIgcmV0ZW50aW9uIGhhcyBzd2VwdFxuICogdGhlIGV2ZW50cyBpdCB3YXMgYnVpbHQgZnJvbS5cbiAqXG4gKiBUV08gUFJPUEVSVElFUyBUSEUgSk9CIEhBUyBUTyBIQVZFLCBhbmQgYm90aCBzaGFwZSB0aGUgY29kZTpcbiAqXG4gKiAgLSBJREVNUE9URU5UIFBFUiBEQVkuIEEgZGF5IGlzIHJlY29tcHV0ZWQgV0hPTEVTQUxFIGFuZCB1cHNlcnRlZCwgbmV2ZXJcbiAqICAgIGluY3JlbWVudGVkLiBJbmNyZW1lbnRpbmcgaXMgaG93IGEgcmVkZWxpdmVyZWQgZXZlbnQsIGEgcmV0cmllZCB0aWNrIG9yIGFcbiAqICAgIGNyYXNoZWQgaGFsZi1ydW4gc2lsZW50bHkgaW5mbGF0ZXMgYSBjb3VudCwgYW5kIGFuIGluZmxhdGVkIHN1Y2Nlc3MgcmF0ZSBpc1xuICogICAgd29yc2UgdGhhbiBhIG1pc3Npbmcgb25lIGJlY2F1c2Ugbm9ib2R5IGNhbiB0ZWxsLlxuICogIC0gUkVCVUlMRFMgQSBEQVkgV0hFTiBMQVRFIEVWRU5UUyBBUlJJVkUuIFRoZSBpbmdlc3QncyBkZWFkLWxldHRlciBxdWV1ZSBpc1xuICogICAgcmVkcml2ZW4gaG91cnMgbGF0ZXIsIHNvIHllc3RlcmRheSdzIG51bWJlcnMgbGVnaXRpbWF0ZWx5IGNoYW5nZS4gVGhlXG4gKiAgICBgY29tcHV0ZWRfYXRgIHN0YW1wIGlzIHdoYXQgbWFrZXMgdGhhdCB2aXNpYmxlIHJhdGhlciB0aGFuIG15c3RlcmlvdXMuXG4gKlxuICogVEhFIFNFVFRMRSBERUxBWSBsaXZlcyBoZXJlIHRvbzogYSBkYXkgaXMgb25seSByb2xsZWQgdXAgb25jZSBpdCBoYXMgYmVlbiBjbG9zZWRcbiAqIGxvbmcgZW5vdWdoIGZvciBpbi1mbGlnaHQgZXhlY3V0aW9ucyB0byBmaW5pc2guIFJvbGxpbmcgdXAgYSBkYXkgYXQgMDA6MDEgY291bnRzXG4gKiBldmVyeSBleGVjdXRpb24gc3RpbGwgcnVubmluZyBhdCBtaWRuaWdodCBhcyBuZWl0aGVyIGEgc3VjY2VzcyBub3IgYSBmYWlsdXJlLlxuICovXG5cbmltcG9ydCB7IGNyZWF0ZUxvZ2dlciwgZXJyb3JNZXNzYWdlLCBlbnZJbnQgfSBmcm9tICdAcGlwZWxpbmUtYnVpbGRlci9hcGktY29yZSc7XG5pbXBvcnQgeyBzcWwgfSBmcm9tICdkcml6emxlLW9ybSc7XG5pbXBvcnQgeyB3aXRoVGVuYW50VHggfSBmcm9tICcuLi8uLi9kYXRhYmFzZS90ZW5hbmN5LmpzJztcblxuY29uc3QgbG9nZ2VyID0gY3JlYXRlTG9nZ2VyKCdkYWlseS1yb2xsdXAnKTtcblxuLyoqXG4gKiBIb3cgbG9uZyBhZnRlciBhIGRheSBjbG9zZXMgYmVmb3JlIGl0IGlzIHJvbGxlZCB1cCAoaG91cnMpLlxuICpcbiAqIFNpeCBieSBkZWZhdWx0LCBtYXRjaGluZyB0aGUgcmVwb3J0IHNjaGVkdWxlcidzIG93biBzZXR0bGUgZGVsYXk6IGJvdGggZXhpc3QgZm9yXG4gKiB0aGUgc2FtZSByZWFzb24sIGFuZCBhIHJvbGx1cCB0aGF0IGxhZ2dlZCB0aGUgcmVwb3J0IHdvdWxkIG1ha2UgYSBzY2hlZHVsZWRcbiAqIHJlcG9ydCByZWFkIGEgaGFsZi1idWlsdCBkYXkuXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiByb2xsdXBTZXR0bGVIb3VycygpOiBudW1iZXIge1xuICByZXR1cm4gZW52SW50KCdSRVBPUlRJTkdfUk9MTFVQX1NFVFRMRV9IT1VSUycsIDYsIHsgbWluOiAwIH0pO1xufVxuXG4vKiogUm93cyBwZXIgcmVidWlsZCBwYXNzLiBCb3VuZHMgdGhlIHN0YXRlbWVudCwgbm90IHRoZSBiYWNrbG9nLiAqL1xuZXhwb3J0IGZ1bmN0aW9uIHJvbGx1cEJhdGNoRGF5cygpOiBudW1iZXIge1xuICByZXR1cm4gZW52SW50KCdSRVBPUlRJTkdfUk9MTFVQX0JBVENIX0RBWVMnLCA3LCB7IG1pbjogMSB9KTtcbn1cblxuLyoqIFRoZSBVVEMgbWlkbmlnaHQgdGhhdCBzdGFydHMgYGRgJ3MgZGF5LiAqL1xuZXhwb3J0IGZ1bmN0aW9uIHV0Y0RheVN0YXJ0KGQ6IERhdGUpOiBEYXRlIHtcbiAgcmV0dXJuIG5ldyBEYXRlKERhdGUuVVRDKGQuZ2V0VVRDRnVsbFllYXIoKSwgZC5nZXRVVENNb250aCgpLCBkLmdldFVUQ0RhdGUoKSkpO1xufVxuXG4vKipcbiAqIFJlYnVpbGQgdGhlIHJvbGx1cCByb3dzIGZvciBvbmUgVVRDIGRheSwgZm9yIGV2ZXJ5IG9yZyB0aGF0IGhhZCBhY3Rpdml0eS5cbiAqXG4gKiBPTkUgc3RhdGVtZW50LCBjb21wdXRlZCBlbnRpcmVseSBpbiBQb3N0Z3Jlcy4gRGVsaWJlcmF0ZWx5IG5vdCBcInNlbGVjdCB0aGVcbiAqIGV2ZW50cywgYWdncmVnYXRlIGluIE5vZGUsIHdyaXRlIHRoZSByb3dzXCI6IHRoZSB3aG9sZSBwb2ludCBvZiB0aGUgcm9sbHVwIGlzIHRvXG4gKiBhdm9pZCBtb3ZpbmcgYSBkYXkncyBldmVudHMgYWNyb3NzIHRoZSB3aXJlLCBhbmQgYSBqb2IgdGhhdCBkaWQgaXQgdG8gQlVJTEQgdGhlXG4gKiByb2xsdXAgd291bGQganVzdCBtb3ZlIHRoZSBjb3N0IHJhdGhlciB0aGFuIHJlbW92ZSBpdC5cbiAqXG4gKiBQZXJjZW50aWxlcyB1c2UgYFBFUkNFTlRJTEVfQ09OVGAsIHdoaWNoIGlzIHRoZSBzYW1lIGZ1bmN0aW9uIHRoZSBvbi1kZW1hbmRcbiAqIHF1ZXJpZXMgdXNlIOKAlCBzbyBhIHJvbGx1cC1iYWNrZWQgbnVtYmVyIGFuZCBhIGxpdmUgbnVtYmVyIGFncmVlIHJhdGhlciB0aGFuXG4gKiBkaWZmZXJpbmcgYnkgYW4gaW50ZXJwb2xhdGlvbiBydWxlLlxuICovXG5leHBvcnQgYXN5bmMgZnVuY3Rpb24gcmVidWlsZERheShkYXk6IERhdGUpOiBQcm9taXNlPG51bWJlcj4ge1xuICBjb25zdCBkYXlTdGFydCA9IHV0Y0RheVN0YXJ0KGRheSk7XG4gIGNvbnN0IGRheUVuZCA9IG5ldyBEYXRlKGRheVN0YXJ0LmdldFRpbWUoKSArIDI0ICogMzYwMF8wMDApO1xuXG4gIC8vIFRXTyBHUkFJTlMsIG9uZSBzY2FuLCBhbmQgbm8gR1JPVVBJTkcgU0VUUyBuZWVkZWQ6IGEgUElQRUxJTkUgZXZlbnQgY2Fycmllc1xuICAvLyBubyBzdGFnZSBuYW1lLCBzbyBpdCBjb2xsYXBzZXMgdG8gdGhlIGAnJ2AgcGlwZWxpbmUtbGV2ZWwgcm93LCBhbmQgYSBTVEFHRVxuICAvLyBldmVudCBsYW5kcyBvbiBpdHMgb3duIHN0YWdlIHJvdy4gVGhlIGV2ZW50IHNoYXBlIGdpdmVzIHRoZSBoaWVyYXJjaHkgZm9yXG4gIC8vIGZyZWUuXG4gIC8vXG4gIC8vIENURXMgcmF0aGVyIHRoYW4gd2luZG93IGZ1bmN0aW9ucywgYmVjYXVzZSBgUEVSQ0VOVElMRV9DT05UYCBpcyBhbiBvcmRlcmVkLXNldFxuICAvLyBhZ2dyZWdhdGUgYW5kIGNhbm5vdCBiZSB1c2VkIGFzIGEgd2luZG93IGZ1bmN0aW9uIGF0IGFsbCDigJQgdGhlIHNoYXBlIHRoYXRcbiAgLy8gbG9va3MgbmF0dXJhbCBoZXJlIGRvZXMgbm90IHJ1bi5cbiAgY29uc3QgcmVzdWx0ID0gYXdhaXQgd2l0aFRlbmFudFR4KCh0eCkgPT4gdHguZXhlY3V0ZShzcWxgXG4gICAgV0lUSCBldiBBUyAoXG4gICAgICBTRUxFQ1RcbiAgICAgICAgb3JnX2lkLFxuICAgICAgICBwaXBlbGluZV9pZDo6dGV4dCBBUyBwaXBlbGluZV9pZCxcbiAgICAgICAgQ09BTEVTQ0Uoc3RhZ2VfbmFtZSwgJycpIEFTIHN0YWdlX25hbWUsXG4gICAgICAgIHN0YXR1cyxcbiAgICAgICAgZmFpbHVyZV9jYXRlZ29yeSxcbiAgICAgICAgZHVyYXRpb25fbXNcbiAgICAgIEZST00gcGlwZWxpbmVfZXZlbnRzXG4gICAgICBXSEVSRSBjcmVhdGVkX2F0ID49ICR7ZGF5U3RhcnR9IEFORCBjcmVhdGVkX2F0IDwgJHtkYXlFbmR9XG4gICAgICAgIEFORCBwaXBlbGluZV9pZCBJUyBOT1QgTlVMTFxuICAgICAgICBBTkQgZXZlbnRfdHlwZSBJTiAoJ1BJUEVMSU5FJywgJ1NUQUdFJylcbiAgICApLFxuICAgIGNhdHMgQVMgKFxuICAgICAgU0VMRUNUIG9yZ19pZCwgcGlwZWxpbmVfaWQsIHN0YWdlX25hbWUsXG4gICAgICAgICAgICAganNvbmJfb2JqZWN0X2FnZyhmYWlsdXJlX2NhdGVnb3J5LCBuKSBBUyBmYWlsdXJlc19ieV9jYXRlZ29yeVxuICAgICAgRlJPTSAoXG4gICAgICAgIFNFTEVDVCBvcmdfaWQsIHBpcGVsaW5lX2lkLCBzdGFnZV9uYW1lLCBmYWlsdXJlX2NhdGVnb3J5LCBDT1VOVCgqKTo6aW50IEFTIG5cbiAgICAgICAgRlJPTSBldlxuICAgICAgICBXSEVSRSBmYWlsdXJlX2NhdGVnb3J5IElTIE5PVCBOVUxMXG4gICAgICAgIEdST1VQIEJZIDEsIDIsIDMsIDRcbiAgICAgICkgdFxuICAgICAgR1JPVVAgQlkgMSwgMiwgM1xuICAgICksXG4gICAgYWdnIEFTIChcbiAgICAgIFNFTEVDVFxuICAgICAgICBvcmdfaWQsIHBpcGVsaW5lX2lkLCBzdGFnZV9uYW1lLFxuICAgICAgICBDT1VOVCgqKTo6aW50IEFTIHJ1bnMsXG4gICAgICAgIENPVU5UKCopIEZJTFRFUiAoV0hFUkUgc3RhdHVzIElOICgnU1VDQ0VFREVEJywgJ1NVQ0NFU1MnKSk6OmludCBBUyBzdWNjZWVkZWQsXG4gICAgICAgIENPVU5UKCopIEZJTFRFUiAoV0hFUkUgc3RhdHVzIElOICgnRkFJTEVEJywgJ0ZBSUxVUkUnLCAnRVJST1InLCAnVElNRURfT1VUJykpOjppbnQgQVMgZmFpbGVkLFxuICAgICAgICAtLSBOVUxMIGR1cmF0aW9ucyBhcmUgaWdub3JlZCBieSBQRVJDRU5USUxFX0NPTlQsIHdoaWNoIGlzIHdoYXQgd2Ugd2FudDogYW5cbiAgICAgICAgLS0gZXZlbnQgd2l0aCBubyBkdXJhdGlvbiBoYXMgbm8gdGltaW5nIHRvIGNvbnRyaWJ1dGUsIGFuZCB0cmVhdGluZyBpdCBhc1xuICAgICAgICAtLSB6ZXJvIHdvdWxkIGRyYWcgZXZlcnkgcGVyY2VudGlsZSBkb3duLlxuICAgICAgICBQRVJDRU5USUxFX0NPTlQoMC41KSBXSVRISU4gR1JPVVAgKE9SREVSIEJZIGR1cmF0aW9uX21zKTo6aW50IEFTIHA1MF9tcyxcbiAgICAgICAgUEVSQ0VOVElMRV9DT05UKDAuOSkgV0lUSElOIEdST1VQIChPUkRFUiBCWSBkdXJhdGlvbl9tcyk6OmludCBBUyBwOTBfbXMsXG4gICAgICAgIFBFUkNFTlRJTEVfQ09OVCgwLjk1KSBXSVRISU4gR1JPVVAgKE9SREVSIEJZIGR1cmF0aW9uX21zKTo6aW50IEFTIHA5NV9tcyxcbiAgICAgICAgKFNVTShDT0FMRVNDRShkdXJhdGlvbl9tcywgMCkpIC8gMTAwMCk6OmludCBBUyBidWlsZF9zZWNvbmRzXG4gICAgICBGUk9NIGV2XG4gICAgICBHUk9VUCBCWSAxLCAyLCAzXG4gICAgKVxuICAgIElOU0VSVCBJTlRPIGV4ZWN1dGlvbl9kYWlseV9yb2xsdXBzIChcbiAgICAgIG9yZ19pZCwgZGF5LCBwaXBlbGluZV9pZCwgc3RhZ2VfbmFtZSxcbiAgICAgIHJ1bnMsIHN1Y2NlZWRlZCwgZmFpbGVkLCBmYWlsdXJlc19ieV9jYXRlZ29yeSxcbiAgICAgIHA1MF9tcywgcDkwX21zLCBwOTVfbXMsIGJ1aWxkX3NlY29uZHMsIGNvbXB1dGVkX2F0XG4gICAgKVxuICAgIFNFTEVDVFxuICAgICAgYS5vcmdfaWQsICR7ZGF5U3RhcnR9Ojp0aW1lc3RhbXB0eiwgYS5waXBlbGluZV9pZCwgYS5zdGFnZV9uYW1lLFxuICAgICAgYS5ydW5zLCBhLnN1Y2NlZWRlZCwgYS5mYWlsZWQsXG4gICAgICBDT0FMRVNDRShjLmZhaWx1cmVzX2J5X2NhdGVnb3J5LCAne30nOjpqc29uYiksXG4gICAgICBhLnA1MF9tcywgYS5wOTBfbXMsIGEucDk1X21zLCBhLmJ1aWxkX3NlY29uZHMsIE5PVygpXG4gICAgRlJPTSBhZ2cgYVxuICAgIExFRlQgSk9JTiBjYXRzIGNcbiAgICAgIE9OIGMub3JnX2lkID0gYS5vcmdfaWQgQU5EIGMucGlwZWxpbmVfaWQgPSBhLnBpcGVsaW5lX2lkIEFORCBjLnN0YWdlX25hbWUgPSBhLnN0YWdlX25hbWVcbiAgICBPTiBDT05GTElDVCAob3JnX2lkLCBkYXksIHBpcGVsaW5lX2lkLCBzdGFnZV9uYW1lKSBETyBVUERBVEUgU0VUXG4gICAgICBydW5zID0gZXhjbHVkZWQucnVucyxcbiAgICAgIHN1Y2NlZWRlZCA9IGV4Y2x1ZGVkLnN1Y2NlZWRlZCxcbiAgICAgIGZhaWxlZCA9IGV4Y2x1ZGVkLmZhaWxlZCxcbiAgICAgIGZhaWx1cmVzX2J5X2NhdGVnb3J5ID0gZXhjbHVkZWQuZmFpbHVyZXNfYnlfY2F0ZWdvcnksXG4gICAgICBwNTBfbXMgPSBleGNsdWRlZC5wNTBfbXMsXG4gICAgICBwOTBfbXMgPSBleGNsdWRlZC5wOTBfbXMsXG4gICAgICBwOTVfbXMgPSBleGNsdWRlZC5wOTVfbXMsXG4gICAgICBidWlsZF9zZWNvbmRzID0gZXhjbHVkZWQuYnVpbGRfc2Vjb25kcyxcbiAgICAgIGNvbXB1dGVkX2F0ID0gZXhjbHVkZWQuY29tcHV0ZWRfYXRcbiAgYCkpO1xuICBjb25zdCByb3dzID0gKHJlc3VsdCBhcyB1bmtub3duIGFzIHsgcm93Q291bnQ/OiBudW1iZXIgfSkucm93Q291bnQgPz8gMDtcbiAgbG9nZ2VyLmRlYnVnKCdSb2xsZWQgdXAgYSBkYXknLCB7IGRheTogZGF5U3RhcnQudG9JU09TdHJpbmcoKS5zbGljZSgwLCAxMCksIHJvd3MgfSk7XG4gIHJldHVybiByb3dzO1xufVxuXG4vKipcbiAqIE9uZSByb2xsdXAgcGFzczogcmVidWlsZCB0aGUgbW9zdCByZWNlbnQgc2V0dGxlZCBkYXlzLlxuICpcbiAqIFJlYnVpbGRpbmcgYSBXSU5ET1cgcmF0aGVyIHRoYW4gb25seSB0aGUgbmV3ZXN0IGRheSBpcyB3aGF0IGhhbmRsZXMgbGF0ZVxuICogZXZlbnRzOiB0aGUgZGVhZC1sZXR0ZXIgcmVkcml2ZSBsYW5kcyBldmVudHMgZm9yIGRheXMgYWxyZWFkeSByb2xsZWQgdXAsIGFuZCBhXG4gKiBqb2IgdGhhdCBvbmx5IGV2ZXIgYnVpbHQgXCJ5ZXN0ZXJkYXlcIiB3b3VsZCBsZWF2ZSB0aG9zZSBkYXlzIHBlcm1hbmVudGx5IHdyb25nLlxuICogUmVjb21wdXRpbmcgYSB3ZWVrIGNvc3RzIG9uZSBzdGF0ZW1lbnQgcGVyIGRheSBhbmQgbWFrZXMgbGF0ZW5lc3Mgc2VsZi1oZWFsaW5nXG4gKiBpbnN0ZWFkIG9mIGEgc3VwcG9ydCBxdWVzdGlvbi5cbiAqXG4gKiBOZXZlciB0aHJvd3Mg4oCUIHRoZSBjYWxsZXIgaXMgYSBzY2hlZHVsZXIgdGljaywgYW5kIGEgZmFpbGVkIHBhc3MgbXVzdCByZXRyeSBuZXh0XG4gKiB0aWNrIHJhdGhlciB0aGFuIHRha2UgdGhlIHNlcnZpY2UncyBiYWNrZ3JvdW5kIHdvcmsgZG93biB3aXRoIGl0LlxuICovXG5leHBvcnQgYXN5bmMgZnVuY3Rpb24gcnVuUm9sbHVwUGFzcyhub3c6IERhdGUgPSBuZXcgRGF0ZSgpKTogUHJvbWlzZTx7IGRheXM6IG51bWJlcjsgcm93czogbnVtYmVyIH0+IHtcbiAgY29uc3Qgc2V0dGxlTXMgPSByb2xsdXBTZXR0bGVIb3VycygpICogMzYwMF8wMDA7XG4gIC8vIFRoZSBuZXdlc3QgZGF5IHRoYXQgaGFzIHNldHRsZWQuIEEgZGF5IGlzIG9ubHkgYnVpbHQgb25jZSBldmVyeXRoaW5nIHRoYXRcbiAgLy8gc3RhcnRlZCBpbiBpdCBoYXMgaGFkIHRpbWUgdG8gZmluaXNoLlxuICBjb25zdCBuZXdlc3QgPSB1dGNEYXlTdGFydChuZXcgRGF0ZShub3cuZ2V0VGltZSgpIC0gc2V0dGxlTXMpKTtcbiAgbGV0IHJvd3MgPSAwO1xuICBsZXQgZGF5cyA9IDA7XG4gIGZvciAobGV0IGkgPSAwOyBpIDwgcm9sbHVwQmF0Y2hEYXlzKCk7IGkrKykge1xuICAgIGNvbnN0IGRheSA9IG5ldyBEYXRlKG5ld2VzdC5nZXRUaW1lKCkgLSBpICogMjQgKiAzNjAwXzAwMCk7XG4gICAgdHJ5IHtcbiAgICAgIHJvd3MgKz0gYXdhaXQgcmVidWlsZERheShkYXkpO1xuICAgICAgZGF5cysrO1xuICAgIH0gY2F0Y2ggKGVycikge1xuICAgICAgbG9nZ2VyLndhcm4oJ1JvbGx1cCBwYXNzIGZhaWxlZCBmb3IgYSBkYXk7IGl0IHdpbGwgYmUgcmV0cmllZCBuZXh0IHBhc3MnLCB7XG4gICAgICAgIGRheTogZGF5LnRvSVNPU3RyaW5nKCkuc2xpY2UoMCwgMTApLFxuICAgICAgICBlcnJvcjogZXJyb3JNZXNzYWdlKGVyciksXG4gICAgICB9KTtcbiAgICB9XG4gIH1cbiAgcmV0dXJuIHsgZGF5cywgcm93cyB9O1xufVxuIl19
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { LeadTimeGap } from '../../database/schema/reporting-analytics.js';
|
|
2
|
+
/** The fields of an inserted event row this module needs. */
|
|
3
|
+
export interface DeployCandidate {
|
|
4
|
+
orgId: string;
|
|
5
|
+
pipelineId: string | null;
|
|
6
|
+
executionId: string | null;
|
|
7
|
+
eventType: string;
|
|
8
|
+
status: string;
|
|
9
|
+
stageName: string | null;
|
|
10
|
+
environment: string | null;
|
|
11
|
+
commitSha?: string | null;
|
|
12
|
+
commitTimestamp?: Date | null;
|
|
13
|
+
commitCount?: number | null;
|
|
14
|
+
completedAt?: Date | null;
|
|
15
|
+
startedAt?: Date | null;
|
|
16
|
+
createdAt?: Date | null;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Is this row a deploy?
|
|
20
|
+
*
|
|
21
|
+
* A STAGE event with an environment, in a terminal state. `IN_PROGRESS` rows are
|
|
22
|
+
* skipped deliberately: a deploy that has started has no duration, no outcome and
|
|
23
|
+
* no place in a deployment-frequency count, and writing it would make every
|
|
24
|
+
* in-flight deploy look like a successful one until its next event landed.
|
|
25
|
+
*/
|
|
26
|
+
export declare function isDeployEvent(row: DeployCandidate): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Why lead time is absent for a deploy that has no commit timestamp.
|
|
29
|
+
*
|
|
30
|
+
* `no_commit_data` is the honest default: the forwarder resolves the commit range
|
|
31
|
+
* in-account and reports the reason it could not, so anything else here would be
|
|
32
|
+
* this module guessing. The reason it DID report rides on the event's `detail`.
|
|
33
|
+
*/
|
|
34
|
+
export declare function leadTimeGapOf(row: DeployCandidate & {
|
|
35
|
+
detail?: Record<string, unknown> | null;
|
|
36
|
+
}): LeadTimeGap | null;
|
|
37
|
+
/**
|
|
38
|
+
* Upsert the deploy rows for a batch of freshly-inserted events.
|
|
39
|
+
*
|
|
40
|
+
* ONE ROW PER (execution, environment), so a redelivered event refreshes the row
|
|
41
|
+
* rather than adding a second deploy — the same at-least-once problem the event
|
|
42
|
+
* insert solves with its partial unique index.
|
|
43
|
+
*
|
|
44
|
+
* The conflict update deliberately does NOT touch `failed_at` / `restored_at`:
|
|
45
|
+
* those are set by the post-deploy outcome markers and by incident correlation,
|
|
46
|
+
* and a late redelivery of the original deploy event must not erase a recorded
|
|
47
|
+
* production failure.
|
|
48
|
+
*
|
|
49
|
+
* Never throws. An ingest batch that landed its events has done the durable part
|
|
50
|
+
* of its job; failing the whole batch because a derived row could not be written
|
|
51
|
+
* would turn a rollup bug into data loss, and the backfill can rebuild these from
|
|
52
|
+
* the events while they exist.
|
|
53
|
+
*/
|
|
54
|
+
export declare function upsertDeployments(rows: ReadonlyArray<DeployCandidate & {
|
|
55
|
+
detail?: Record<string, unknown> | null;
|
|
56
|
+
}>): Promise<number>;
|
|
57
|
+
/**
|
|
58
|
+
* Record a post-deploy outcome against the deploy it concerns.
|
|
59
|
+
*
|
|
60
|
+
* Denormalized onto `dora_deployments` so change-failure rate and
|
|
61
|
+
* time-to-restore are one scan over the window instead of a join per read. The
|
|
62
|
+
* `deployment_outcomes` table stays the authority — this is the copy the
|
|
63
|
+
* aggregates use, and it is rebuildable from it.
|
|
64
|
+
*/
|
|
65
|
+
export declare function applyDeployOutcome(orgId: string, executionId: string, outcome: 'failed' | 'restored', at: Date, environment?: string): Promise<void>;
|
|
66
|
+
/**
|
|
67
|
+
* Attribute an incident to the most recent successful deploy to its environment
|
|
68
|
+
* within the correlation window, and label how confident that link is.
|
|
69
|
+
*
|
|
70
|
+
* The confidence is the point. "This deploy broke production" and "an incident
|
|
71
|
+
* opened eleven hours after this deploy, and the window happens to be twelve" are
|
|
72
|
+
* different claims, and a manager acts differently on each — so the report shows
|
|
73
|
+
* which one it is rather than presenting both as a change failure.
|
|
74
|
+
*/
|
|
75
|
+
export declare function correlateIncident(orgId: string, environment: string, openedAt: Date, windowHours: number): Promise<{
|
|
76
|
+
executionId: string;
|
|
77
|
+
confidence: 'high' | 'medium' | 'low';
|
|
78
|
+
} | null>;
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
// Copyright 2026 Pipeline Builder Contributors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* Writing `dora_deployments` at ingest.
|
|
5
|
+
*
|
|
6
|
+
* Every DORA number used to be derived by scanning `pipeline_events` per read.
|
|
7
|
+
* Two things were wrong with that, and only one of them was speed:
|
|
8
|
+
*
|
|
9
|
+
* 1. LEAD TIME WAS DOUBLE-COUNTED IN WAITING. A deploy emits a STAGE event and
|
|
10
|
+
* an ACTION event, and both carry the same resolved commit range. Any
|
|
11
|
+
* aggregate over events therefore counted one deploy's lead time twice the
|
|
12
|
+
* moment somebody added the ACTION rows to a query. Storing it once per
|
|
13
|
+
* execution makes that mistake unavailable rather than merely unmade.
|
|
14
|
+
* 2. THE NUMBERS DID NOT SURVIVE RETENTION. The sweep purges raw events by
|
|
15
|
+
* tier, so a quarterly report could not be recomputed once its events were
|
|
16
|
+
* gone — which is exactly when someone asks about it.
|
|
17
|
+
*
|
|
18
|
+
* A deploy is a STAGE event carrying an `environment`: the same predicate the
|
|
19
|
+
* DORA queries have always used, now applied once at write time.
|
|
20
|
+
*/
|
|
21
|
+
import { createLogger, errorMessage } from '@pipeline-builder/api-core';
|
|
22
|
+
import { and, eq, isNull, sql } from 'drizzle-orm';
|
|
23
|
+
import { schema } from '../../database/drizzle-schema.js';
|
|
24
|
+
import { withTenantTx } from '../../database/tenancy.js';
|
|
25
|
+
const logger = createLogger('deploy-rollup');
|
|
26
|
+
/** Statuses an event uses for "this finished, and it worked". */
|
|
27
|
+
const SUCCEEDED = new Set(['SUCCEEDED', 'SUCCESS']);
|
|
28
|
+
/** …and for "this finished, and it did not". */
|
|
29
|
+
const FAILED = new Set(['FAILED', 'FAILURE', 'CANCELED', 'CANCELLED', 'STOPPED', 'TIMED_OUT']);
|
|
30
|
+
/**
|
|
31
|
+
* Is this row a deploy?
|
|
32
|
+
*
|
|
33
|
+
* A STAGE event with an environment, in a terminal state. `IN_PROGRESS` rows are
|
|
34
|
+
* skipped deliberately: a deploy that has started has no duration, no outcome and
|
|
35
|
+
* no place in a deployment-frequency count, and writing it would make every
|
|
36
|
+
* in-flight deploy look like a successful one until its next event landed.
|
|
37
|
+
*/
|
|
38
|
+
export function isDeployEvent(row) {
|
|
39
|
+
return row.eventType === 'STAGE'
|
|
40
|
+
&& !!row.environment
|
|
41
|
+
&& !!row.executionId
|
|
42
|
+
&& (SUCCEEDED.has(row.status) || FAILED.has(row.status));
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Why lead time is absent for a deploy that has no commit timestamp.
|
|
46
|
+
*
|
|
47
|
+
* `no_commit_data` is the honest default: the forwarder resolves the commit range
|
|
48
|
+
* in-account and reports the reason it could not, so anything else here would be
|
|
49
|
+
* this module guessing. The reason it DID report rides on the event's `detail`.
|
|
50
|
+
*/
|
|
51
|
+
export function leadTimeGapOf(row) {
|
|
52
|
+
if (row.commitTimestamp)
|
|
53
|
+
return null;
|
|
54
|
+
const reported = row.detail?.leadTimeGap;
|
|
55
|
+
if (reported === 'no_token' || reported === 'rate_limited' || reported === 'unsupported_source')
|
|
56
|
+
return reported;
|
|
57
|
+
return 'no_commit_data';
|
|
58
|
+
}
|
|
59
|
+
/** When the deploy finished. Falls back through the event's own timestamps. */
|
|
60
|
+
function deployedAtOf(row) {
|
|
61
|
+
return row.completedAt ?? row.startedAt ?? row.createdAt ?? null;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Upsert the deploy rows for a batch of freshly-inserted events.
|
|
65
|
+
*
|
|
66
|
+
* ONE ROW PER (execution, environment), so a redelivered event refreshes the row
|
|
67
|
+
* rather than adding a second deploy — the same at-least-once problem the event
|
|
68
|
+
* insert solves with its partial unique index.
|
|
69
|
+
*
|
|
70
|
+
* The conflict update deliberately does NOT touch `failed_at` / `restored_at`:
|
|
71
|
+
* those are set by the post-deploy outcome markers and by incident correlation,
|
|
72
|
+
* and a late redelivery of the original deploy event must not erase a recorded
|
|
73
|
+
* production failure.
|
|
74
|
+
*
|
|
75
|
+
* Never throws. An ingest batch that landed its events has done the durable part
|
|
76
|
+
* of its job; failing the whole batch because a derived row could not be written
|
|
77
|
+
* would turn a rollup bug into data loss, and the backfill can rebuild these from
|
|
78
|
+
* the events while they exist.
|
|
79
|
+
*/
|
|
80
|
+
export async function upsertDeployments(rows) {
|
|
81
|
+
const deploys = rows.filter(isDeployEvent);
|
|
82
|
+
if (deploys.length === 0)
|
|
83
|
+
return 0;
|
|
84
|
+
const values = deploys.flatMap((row) => {
|
|
85
|
+
const deployedAt = deployedAtOf(row);
|
|
86
|
+
if (!deployedAt)
|
|
87
|
+
return [];
|
|
88
|
+
const succeeded = SUCCEEDED.has(row.status);
|
|
89
|
+
const earliestCommitAt = row.commitTimestamp ?? null;
|
|
90
|
+
// Lead time is commit → deploy, and only when both ends are real. A negative
|
|
91
|
+
// gap means the two timestamps came from different clocks; it is dropped
|
|
92
|
+
// rather than clamped to zero, because "0 seconds" is a claim and "unknown"
|
|
93
|
+
// is the truth.
|
|
94
|
+
const leadSeconds = earliestCommitAt
|
|
95
|
+
? Math.round((deployedAt.getTime() - earliestCommitAt.getTime()) / 1000)
|
|
96
|
+
: null;
|
|
97
|
+
return [{
|
|
98
|
+
orgId: row.orgId,
|
|
99
|
+
executionId: row.executionId,
|
|
100
|
+
environment: row.environment,
|
|
101
|
+
pipelineId: row.pipelineId,
|
|
102
|
+
deployedAt,
|
|
103
|
+
succeeded,
|
|
104
|
+
earliestCommitAt,
|
|
105
|
+
leadTimeSeconds: leadSeconds !== null && leadSeconds >= 0 ? leadSeconds : null,
|
|
106
|
+
commitCount: row.commitCount ?? null,
|
|
107
|
+
commitSha: row.commitSha ?? null,
|
|
108
|
+
leadTimeGap: leadTimeGapOf(row),
|
|
109
|
+
}];
|
|
110
|
+
});
|
|
111
|
+
if (values.length === 0)
|
|
112
|
+
return 0;
|
|
113
|
+
try {
|
|
114
|
+
const landed = await withTenantTx((tx) => tx.insert(schema.doraDeployment).values(values)
|
|
115
|
+
.onConflictDoUpdate({
|
|
116
|
+
target: [schema.doraDeployment.executionId, schema.doraDeployment.environment],
|
|
117
|
+
set: {
|
|
118
|
+
succeeded: sql `excluded.succeeded`,
|
|
119
|
+
deployedAt: sql `excluded.deployed_at`,
|
|
120
|
+
pipelineId: sql `COALESCE(excluded.pipeline_id, ${schema.doraDeployment.pipelineId})`,
|
|
121
|
+
// A later event may resolve a commit range the first one could not, so
|
|
122
|
+
// these fill in — but never blank out what was already measured.
|
|
123
|
+
earliestCommitAt: sql `COALESCE(excluded.earliest_commit_at, ${schema.doraDeployment.earliestCommitAt})`,
|
|
124
|
+
leadTimeSeconds: sql `COALESCE(excluded.lead_time_seconds, ${schema.doraDeployment.leadTimeSeconds})`,
|
|
125
|
+
commitCount: sql `COALESCE(excluded.commit_count, ${schema.doraDeployment.commitCount})`,
|
|
126
|
+
commitSha: sql `COALESCE(excluded.commit_sha, ${schema.doraDeployment.commitSha})`,
|
|
127
|
+
// The gap is cleared once a commit timestamp arrives: a resolved lead
|
|
128
|
+
// time and a reason it could not be resolved must never coexist.
|
|
129
|
+
leadTimeGap: sql `CASE WHEN COALESCE(excluded.earliest_commit_at, ${schema.doraDeployment.earliestCommitAt}) IS NOT NULL
|
|
130
|
+
THEN NULL ELSE excluded.lead_time_gap END`,
|
|
131
|
+
updatedAt: new Date(),
|
|
132
|
+
},
|
|
133
|
+
})
|
|
134
|
+
.returning({ executionId: schema.doraDeployment.executionId }));
|
|
135
|
+
return landed.length;
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
logger.warn('Deploy rollup upsert failed; the events landed and a rebuild can recover it', {
|
|
139
|
+
deploys: values.length,
|
|
140
|
+
error: errorMessage(err),
|
|
141
|
+
});
|
|
142
|
+
return 0;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Record a post-deploy outcome against the deploy it concerns.
|
|
147
|
+
*
|
|
148
|
+
* Denormalized onto `dora_deployments` so change-failure rate and
|
|
149
|
+
* time-to-restore are one scan over the window instead of a join per read. The
|
|
150
|
+
* `deployment_outcomes` table stays the authority — this is the copy the
|
|
151
|
+
* aggregates use, and it is rebuildable from it.
|
|
152
|
+
*/
|
|
153
|
+
export async function applyDeployOutcome(orgId, executionId, outcome, at, environment) {
|
|
154
|
+
const where = environment
|
|
155
|
+
? and(eq(schema.doraDeployment.executionId, executionId), eq(schema.doraDeployment.environment, environment), eq(schema.doraDeployment.orgId, orgId))
|
|
156
|
+
: and(eq(schema.doraDeployment.executionId, executionId), eq(schema.doraDeployment.orgId, orgId));
|
|
157
|
+
// A `restored` marker only means something for a deploy that was marked failed;
|
|
158
|
+
// recording a restore with no failure would produce a negative time-to-restore.
|
|
159
|
+
const set = outcome === 'failed'
|
|
160
|
+
? { failedAt: at, updatedAt: new Date() }
|
|
161
|
+
: { restoredAt: at, updatedAt: new Date() };
|
|
162
|
+
await withTenantTx((tx) => tx.update(schema.doraDeployment).set(set)
|
|
163
|
+
.where(outcome === 'restored'
|
|
164
|
+
? and(where, sql `${schema.doraDeployment.failedAt} IS NOT NULL`)
|
|
165
|
+
: where));
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Attribute an incident to the most recent successful deploy to its environment
|
|
169
|
+
* within the correlation window, and label how confident that link is.
|
|
170
|
+
*
|
|
171
|
+
* The confidence is the point. "This deploy broke production" and "an incident
|
|
172
|
+
* opened eleven hours after this deploy, and the window happens to be twelve" are
|
|
173
|
+
* different claims, and a manager acts differently on each — so the report shows
|
|
174
|
+
* which one it is rather than presenting both as a change failure.
|
|
175
|
+
*/
|
|
176
|
+
export async function correlateIncident(orgId, environment, openedAt, windowHours) {
|
|
177
|
+
const windowStart = new Date(openedAt.getTime() - windowHours * 3600_000);
|
|
178
|
+
const rows = await withTenantTx((tx) => tx.select({
|
|
179
|
+
executionId: schema.doraDeployment.executionId,
|
|
180
|
+
deployedAt: schema.doraDeployment.deployedAt,
|
|
181
|
+
}).from(schema.doraDeployment)
|
|
182
|
+
.where(and(eq(schema.doraDeployment.orgId, orgId), eq(schema.doraDeployment.environment, environment), eq(schema.doraDeployment.succeeded, true), sql `${schema.doraDeployment.deployedAt} <= ${openedAt}`, sql `${schema.doraDeployment.deployedAt} >= ${windowStart}`))
|
|
183
|
+
.orderBy(sql `${schema.doraDeployment.deployedAt} DESC`)
|
|
184
|
+
.limit(1));
|
|
185
|
+
const deploy = rows[0];
|
|
186
|
+
if (!deploy)
|
|
187
|
+
return null;
|
|
188
|
+
// Confidence by how early in the window it fell. The thirds are a judgement,
|
|
189
|
+
// and a stated one: an incident within the first third of the window after a
|
|
190
|
+
// deploy is the case the DORA definition is actually about.
|
|
191
|
+
const elapsed = openedAt.getTime() - deploy.deployedAt.getTime();
|
|
192
|
+
const windowMs = windowHours * 3600_000;
|
|
193
|
+
const confidence = elapsed <= windowMs / 3 ? 'high' : elapsed <= (windowMs * 2) / 3 ? 'medium' : 'low';
|
|
194
|
+
await withTenantTx((tx) => tx.update(schema.doraDeployment)
|
|
195
|
+
.set({ failedAt: openedAt, correlationConfidence: confidence, updatedAt: new Date() })
|
|
196
|
+
.where(and(eq(schema.doraDeployment.executionId, deploy.executionId), eq(schema.doraDeployment.environment, environment),
|
|
197
|
+
// Do not overwrite a failure already attributed: the FIRST incident linked
|
|
198
|
+
// to a deploy is the one that made it a change failure, and a later one
|
|
199
|
+
// re-dating it would move time-to-restore.
|
|
200
|
+
isNull(schema.doraDeployment.failedAt))));
|
|
201
|
+
return { executionId: deploy.executionId, confidence };
|
|
202
|
+
}
|
|
203
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZGVwbG95LXJvbGx1cC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3NyYy9hcGkvcmVwb3J0aW5nL2RlcGxveS1yb2xsdXAudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsK0NBQStDO0FBQy9DLHNDQUFzQztBQUV0Qzs7Ozs7Ozs7Ozs7Ozs7Ozs7R0FpQkc7QUFFSCxPQUFPLEVBQUUsWUFBWSxFQUFFLFlBQVksRUFBRSxNQUFNLDRCQUE0QixDQUFDO0FBQ3hFLE9BQU8sRUFBRSxHQUFHLEVBQUUsRUFBRSxFQUFFLE1BQU0sRUFBRSxHQUFHLEVBQUUsTUFBTSxhQUFhLENBQUM7QUFDbkQsT0FBTyxFQUFFLE1BQU0sRUFBRSxNQUFNLGtDQUFrQyxDQUFDO0FBRTFELE9BQU8sRUFBRSxZQUFZLEVBQUUsTUFBTSwyQkFBMkIsQ0FBQztBQUV6RCxNQUFNLE1BQU0sR0FBRyxZQUFZLENBQUMsZUFBZSxDQUFDLENBQUM7QUFtQjdDLGlFQUFpRTtBQUNqRSxNQUFNLFNBQVMsR0FBRyxJQUFJLEdBQUcsQ0FBQyxDQUFDLFdBQVcsRUFBRSxTQUFTLENBQUMsQ0FBQyxDQUFDO0FBQ3BELGdEQUFnRDtBQUNoRCxNQUFNLE1BQU0sR0FBRyxJQUFJLEdBQUcsQ0FBQyxDQUFDLFFBQVEsRUFBRSxTQUFTLEVBQUUsVUFBVSxFQUFFLFdBQVcsRUFBRSxTQUFTLEVBQUUsV0FBVyxDQUFDLENBQUMsQ0FBQztBQUUvRjs7Ozs7OztHQU9HO0FBQ0gsTUFBTSxVQUFVLGFBQWEsQ0FBQyxHQUFvQjtJQUNoRCxPQUFPLEdBQUcsQ0FBQyxTQUFTLEtBQUssT0FBTztXQUMzQixDQUFDLENBQUMsR0FBRyxDQUFDLFdBQVc7V0FDakIsQ0FBQyxDQUFDLEdBQUcsQ0FBQyxXQUFXO1dBQ2pCLENBQUMsU0FBUyxDQUFDLEdBQUcsQ0FBQyxHQUFHLENBQUMsTUFBTSxDQUFDLElBQUksTUFBTSxDQUFDLEdBQUcsQ0FBQyxHQUFHLENBQUMsTUFBTSxDQUFDLENBQUMs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UGlwZWxpbmUgQnVpbGRlciBDb250cmlidXRvcnNcbi8vIFNQRFgtTGljZW5zZS1JZGVudGlmaWVyOiBBcGFjaGUtMi4wXG5cbi8qKlxuICogV3JpdGluZyBgZG9yYV9kZXBsb3ltZW50c2AgYXQgaW5nZXN0LlxuICpcbiAqIEV2ZXJ5IERPUkEgbnVtYmVyIHVzZWQgdG8gYmUgZGVyaXZlZCBieSBzY2FubmluZyBgcGlwZWxpbmVfZXZlbnRzYCBwZXIgcmVhZC5cbiAqIFR3byB0aGluZ3Mgd2VyZSB3cm9uZyB3aXRoIHRoYXQsIGFuZCBvbmx5IG9uZSBvZiB0aGVtIHdhcyBzcGVlZDpcbiAqXG4gKiAgMS4gTEVBRCBUSU1FIFdBUyBET1VCTEUtQ09VTlRFRCBJTiBXQUlUSU5HLiBBIGRlcGxveSBlbWl0cyBhIFNUQUdFIGV2ZW50IGFuZFxuICogICAgIGFuIEFDVElPTiBldmVudCwgYW5kIGJvdGggY2FycnkgdGhlIHNhbWUgcmVzb2x2ZWQgY29tbWl0IHJhbmdlLiBBbnlcbiAqICAgICBhZ2dyZWdhdGUgb3ZlciBldmVudHMgdGhlcmVmb3JlIGNvdW50ZWQgb25lIGRlcGxveSdzIGxlYWQgdGltZSB0d2ljZSB0aGVcbiAqICAgICBtb21lbnQgc29tZWJvZHkgYWRkZWQgdGhlIEFDVElPTiByb3dzIHRvIGEgcXVlcnkuIFN0b3JpbmcgaXQgb25jZSBwZXJcbiAqICAgICBleGVjdXRpb24gbWFrZXMgdGhhdCBtaXN0YWtlIHVuYXZhaWxhYmxlIHJhdGhlciB0aGFuIG1lcmVseSB1bm1hZGUuXG4gKiAgMi4gVEhFIE5VTUJFUlMgRElEIE5PVCBTVVJWSVZFIFJFVEVOVElPTi4gVGhlIHN3ZWVwIHB1cmdlcyByYXcgZXZlbnRzIGJ5XG4gKiAgICAgdGllciwgc28gYSBxdWFydGVybHkgcmVwb3J0IGNvdWxkIG5vdCBiZSByZWNvbXB1dGVkIG9uY2UgaXRzIGV2ZW50cyB3ZXJlXG4gKiAgICAgZ29uZSDigJQgd2hpY2ggaXMgZXhhY3RseSB3aGVuIHNvbWVvbmUgYXNrcyBhYm91dCBpdC5cbiAqXG4gKiBBIGRlcGxveSBpcyBhIFNUQUdFIGV2ZW50IGNhcnJ5aW5nIGFuIGBlbnZpcm9ubWVudGA6IHRoZSBzYW1lIHByZWRpY2F0ZSB0aGVcbiAqIERPUkEgcXVlcmllcyBoYXZlIGFsd2F5cyB1c2VkLCBub3cgYXBwbGllZCBvbmNlIGF0IHdyaXRlIHRpbWUuXG4gKi9cblxuaW1wb3J0IHsgY3JlYXRlTG9nZ2VyLCBlcnJvck1lc3NhZ2UgfSBmcm9tICdAcGlwZWxpbmUtYnVpbGRlci9hcGktY29yZSc7XG5pbXBvcnQgeyBhbmQsIGVxLCBpc051bGwsIHNxbCB9IGZyb20gJ2RyaXp6bGUtb3JtJztcbmltcG9ydCB7IHNjaGVtYSB9IGZyb20gJy4uLy4uL2RhdGFiYXNlL2RyaXp6bGUtc2NoZW1hLmpzJztcbmltcG9ydCB0eXBlIHsgTGVhZFRpbWVHYXAgfSBmcm9tICcuLi8uLi9kYXRhYmFzZS9zY2hlbWEvcmVwb3J0aW5nLWFuYWx5dGljcy5qcyc7XG5pbXBvcnQgeyB3aXRoVGVuYW50VHggfSBmcm9tICcuLi8uLi9kYXRhYmFzZS90ZW5hbmN5LmpzJztcblxuY29uc3QgbG9nZ2VyID0gY3JlYXRlTG9nZ2VyKCdkZXBsb3ktcm9sbHVwJyk7XG5cbi8qKiBUaGUgZmllbGRzIG9mIGFuIGluc2VydGVkIGV2ZW50IHJvdyB0aGlzIG1vZHVsZSBuZWVkcy4gKi9cbmV4cG9ydCBpbnRlcmZhY2UgRGVwbG95Q2FuZGlkYXRlIHtcbiAgb3JnSWQ6IHN0cmluZztcbiAgcGlwZWxpbmVJZDogc3RyaW5nIHwgbnVsbDtcbiAgZXhlY3V0aW9uSWQ6IHN0cmluZyB8IG51bGw7XG4gIGV2ZW50VHlwZTogc3RyaW5nO1xuICBzdGF0dXM6IHN0cmluZztcbiAgc3RhZ2VOYW1lOiBzdHJpbmcgfCBudWxsO1xuICBlbnZpcm9ubWVudDogc3RyaW5nIHwgbnVsbDtcbiAgY29tbWl0U2hhPzogc3RyaW5nIHwgbnVsbDtcbiAgY29tbWl0VGltZXN0YW1wPzogRGF0ZSB8IG51bGw7XG4gIGNvbW1pdENvdW50PzogbnVtYmVyIHwgbnVsbDtcbiAgY29tcGxldGVkQXQ/OiBEYXRlIHwgbnVsbDtcbiAgc3RhcnRlZEF0PzogRGF0ZSB8IG51bGw7XG4gIGNyZWF0ZWRBdD86IERhdGUgfCBudWxsO1xufVxuXG4vKiogU3RhdHVzZXMgYW4gZXZlbnQgdXNlcyBmb3IgXCJ0aGlzIGZpbmlzaGVkLCBhbmQgaXQgd29ya2VkXCIuICovXG5jb25zdCBTVUNDRUVERUQgPSBuZXcgU2V0KFsnU1VDQ0VFREVEJywgJ1NVQ0NFU1MnXSk7XG4vKiog4oCmYW5kIGZvciBcInRoaXMgZmluaXNoZWQsIGFuZCBpdCBkaWQgbm90XCIuICovXG5jb25zdCBGQUlMRUQgPSBuZXcgU2V0KFsnRkFJTEVEJywgJ0ZBSUxVUkUnLCAnQ0FOQ0VMRUQnLCAnQ0FOQ0VMTEVEJywgJ1NUT1BQRUQnLCAnVElNRURfT1VUJ10pO1xuXG4vKipcbiAqIElzIHRoaXMgcm93IGEgZGVwbG95P1xuICpcbiAqIEEgU1RBR0UgZXZlbnQgd2l0aCBhbiBlbnZpcm9ubWVudCwgaW4gYSB0ZXJtaW5hbCBzdGF0ZS4gYElOX1BST0dSRVNTYCByb3dzIGFyZVxuICogc2tpcHBlZCBkZWxpYmVyYXRlbHk6IGEgZGVwbG95IHRoYXQgaGFzIHN0YXJ0ZWQgaGFzIG5vIGR1cmF0aW9uLCBubyBvdXRjb21lIGFuZFxuICogbm8gcGxhY2UgaW4gYSBkZXBsb3ltZW50LWZyZXF1ZW5jeSBjb3VudCwgYW5kIHdyaXRpbmcgaXQgd291bGQgbWFrZSBldmVyeVxuICogaW4tZmxpZ2h0IGRlcGxveSBsb29rIGxpa2UgYSBzdWNjZXNzZnVsIG9uZSB1bnRpbCBpdHMgbmV4dCBldmVudCBsYW5kZWQuXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBpc0RlcGxveUV2ZW50KHJvdzogRGVwbG95Q2FuZGlkYXRlKTogYm9vbGVhbiB7XG4gIHJldHVybiByb3cuZXZlbnRUeXBlID09PSAnU1RBR0UnXG4gICAgJiYgISFyb3cuZW52aXJvbm1lbnRcbiAgICAmJiAhIXJvdy5leGVjdXRpb25JZFxuICAgICYmIChTVUNDRUVERUQuaGFzKHJvdy5zdGF0dXMpIHx8IEZBSUxFRC5oYXMocm93LnN0YXR1cykpO1xufVxuXG4vKipcbiAqIFdoeSBsZWFkIHRpbWUgaXMgYWJzZW50IGZvciBhIGRlcGxveSB0aGF0IGhhcyBubyBjb21taXQgdGltZXN0YW1wLlxuICpcbiAqIGBub19jb21taXRfZGF0YWAgaXMgdGhlIGhvbmVzdCBkZWZhdWx0OiB0aGUgZm9yd2FyZGVyIHJlc29sdmVzIHRoZSBjb21taXQgcmFuZ2VcbiAqIGluLWFjY291bnQgYW5kIHJlcG9ydHMgdGhlIHJlYXNvbiBpdCBjb3VsZCBub3QsIHNvIGFueXRoaW5nIGVsc2UgaGVyZSB3b3VsZCBiZVxuICogdGhpcyBtb2R1bGUgZ3Vlc3NpbmcuIFRoZSByZWFzb24gaXQgRElEIHJlcG9ydCByaWRlcyBvbiB0aGUgZXZlbnQncyBgZGV0YWlsYC5cbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIGxlYWRUaW1lR2FwT2Yocm93OiBEZXBsb3lDYW5kaWRhdGUgJiB7IGRldGFpbD86IFJlY29yZDxzdHJpbmcsIHVua25vd24+IHwgbnVsbCB9KTogTGVhZFRpbWVHYXAgfCBudWxsIHtcbiAgaWYgKHJvdy5jb21taXRUaW1lc3RhbXApIHJldHVybiBudWxsO1xuICBjb25zdCByZXBvcnRlZCA9IHJvdy5kZXRhaWw/LmxlYWRUaW1lR2FwO1xuICBpZiAocmVwb3J0ZWQgPT09ICdub190b2tlbicgfHwgcmVwb3J0ZWQgPT09ICdyYXRlX2xpbWl0ZWQnIHx8IHJlcG9ydGVkID09PSAndW5zdXBwb3J0ZWRfc291cmNlJykgcmV0dXJuIHJlcG9ydGVkO1xuICByZXR1cm4gJ25vX2NvbW1pdF9kYXRhJztcbn1cblxuLyoqIFdoZW4gdGhlIGRlcGxveSBmaW5pc2hlZC4gRmFsbHMgYmFjayB0aHJvdWdoIHRoZSBldmVudCdzIG93biB0aW1lc3RhbXBzLiAqL1xuZnVuY3Rpb24gZGVwbG95ZWRBdE9mKHJvdzogRGVwbG95Q2FuZGlkYXRlKTogRGF0ZSB8IG51bGwge1xuICByZXR1cm4gcm93LmNvbXBsZXRlZEF0ID8/IHJvdy5zdGFydGVkQXQgPz8gcm93LmNyZWF0ZWRBdCA/PyBudWxsO1xufVxuXG4vKipcbiAqIFVwc2VydCB0aGUgZGVwbG95IHJvd3MgZm9yIGEgYmF0Y2ggb2YgZnJlc2hseS1pbnNlcnRlZCBldmVudHMuXG4gKlxuICogT05FIFJPVyBQRVIgKGV4ZWN1dGlvbiwgZW52aXJvbm1lbnQpLCBzbyBhIHJlZGVsaXZlcmVkIGV2ZW50IHJlZnJlc2hlcyB0aGUgcm93XG4gKiByYXRoZXIgdGhhbiBhZGRpbmcgYSBzZWNvbmQgZGVwbG95IOKAlCB0aGUgc2FtZSBhdC1sZWFzdC1vbmNlIHByb2JsZW0gdGhlIGV2ZW50XG4gKiBpbnNlcnQgc29sdmVzIHdpdGggaXRzIHBhcnRpYWwgdW5pcXVlIGluZGV4LlxuICpcbiAqIFRoZSBjb25mbGljdCB1cGRhdGUgZGVsaWJlcmF0ZWx5IGRvZXMgTk9UIHRvdWNoIGBmYWlsZWRfYXRgIC8gYHJlc3RvcmVkX2F0YDpcbiAqIHRob3NlIGFyZSBzZXQgYnkgdGhlIHBvc3QtZGVwbG95IG91dGNvbWUgbWFya2VycyBhbmQgYnkgaW5jaWRlbnQgY29ycmVsYXRpb24sXG4gKiBhbmQgYSBsYXRlIHJlZGVsaXZlcnkgb2YgdGhlIG9yaWdpbmFsIGRlcGxveSBldmVudCBtdXN0IG5vdCBlcmFzZSBhIHJlY29yZGVkXG4gKiBwcm9kdWN0aW9uIGZhaWx1cmUuXG4gKlxuICogTmV2ZXIgdGhyb3dzLiBBbiBpbmdlc3QgYmF0Y2ggdGhhdCBsYW5kZWQgaXRzIGV2ZW50cyBoYXMgZG9uZSB0aGUgZHVyYWJsZSBwYXJ0XG4gKiBvZiBpdHMgam9iOyBmYWlsaW5nIHRoZSB3aG9sZSBiYXRjaCBiZWNhdXNlIGEgZGVyaXZlZCByb3cgY291bGQgbm90IGJlIHdyaXR0ZW5cbiAqIHdvdWxkIHR1cm4gYSByb2xsdXAgYnVnIGludG8gZGF0YSBsb3NzLCBhbmQgdGhlIGJhY2tmaWxsIGNhbiByZWJ1aWxkIHRoZXNlIGZyb21cbiAqIHRoZSBldmVudHMgd2hpbGUgdGhleSBleGlzdC5cbiAqL1xuZXhwb3J0IGFzeW5jIGZ1bmN0aW9uIHVwc2VydERlcGxveW1lbnRzKFxuICByb3dzOiBSZWFkb25seUFycmF5PERlcGxveUNhbmRpZGF0ZSAmIHsgZGV0YWlsPzogUmVjb3JkPHN0cmluZywgdW5rbm93bj4gfCBudWxsIH0+LFxuKTogUHJvbWlzZTxudW1iZXI+IHtcbiAgY29uc3QgZGVwbG95cyA9IHJvd3MuZmlsdGVyKGlzRGVwbG95RXZlbnQpO1xuICBpZiAoZGVwbG95cy5sZW5ndGggPT09IDApIHJldHVybiAwO1xuXG4gIGNvbnN0IHZhbHVlcyA9IGRlcGxveXMuZmxhdE1hcCgocm93KSA9PiB7XG4gICAgY29uc3QgZGVwbG95ZWRBdCA9IGRlcGxveWVkQXRPZihyb3cpO1xuICAgIGlmICghZGVwbG95ZWRBdCkgcmV0dXJuIFtdO1xuICAgIGNvbnN0IHN1Y2NlZWRlZCA9IFNVQ0NFRURFRC5oYXMocm93LnN0YXR1cyk7XG4gICAgY29uc3QgZWFybGllc3RDb21taXRBdCA9IHJvdy5jb21taXRUaW1lc3RhbXAgPz8gbnVsbDtcbiAgICAvLyBMZWFkIHRpbWUgaXMgY29tbWl0IOKGkiBkZXBsb3ksIGFuZCBvbmx5IHdoZW4gYm90aCBlbmRzIGFyZSByZWFsLiBBIG5lZ2F0aXZlXG4gICAgLy8gZ2FwIG1lYW5zIHRoZSB0d28gdGltZXN0YW1wcyBjYW1lIGZyb20gZGlmZmVyZW50IGNsb2NrczsgaXQgaXMgZHJvcHBlZFxuICAgIC8vIHJhdGhlciB0aGFuIGNsYW1wZWQgdG8gemVybywgYmVjYXVzZSBcIjAgc2Vjb25kc1wiIGlzIGEgY2xhaW0gYW5kIFwidW5rbm93blwiXG4gICAgLy8gaXMgdGhlIHRydXRoLlxuICAgIGNvbnN0IGxlYWRTZWNvbmRzID0gZWFybGllc3RDb21taXRBdFxuICAgICAgPyBNYXRoLnJvdW5kKChkZXBsb3llZEF0LmdldFRpbWUoKSAtIGVhcmxpZXN0Q29tbWl0QXQuZ2V0VGltZSgpKSAvIDEwMDApXG4gICAgICA6IG51bGw7XG4gICAgcmV0dXJuIFt7XG4gICAgICBvcmdJZDogcm93Lm9yZ0lkLFxuICAgICAgZXhlY3V0aW9uSWQ6IHJvdy5leGVjdXRpb25JZCBhcyBzdHJpbmcsXG4gICAgICBlbnZpcm9ubWVudDogcm93LmVudmlyb25tZW50IGFzIHN0cmluZyxcbiAgICAgIHBpcGVsaW5lSWQ6IHJvdy5waXBlbGluZUlkLFxuICAgICAgZGVwbG95ZWRBdCxcbiAgICAgIHN1Y2NlZWRlZCxcbiAgICAgIGVhcmxpZXN0Q29tbWl0QXQsXG4gICAgICBsZWFkVGltZVNlY29uZHM6IGxlYWRTZWNvbmRzICE9PSBudWxsICYmIGxlYWRTZWNvbmRzID49IDAgPyBsZWFkU2Vjb25kcyA6IG51bGwsXG4gICAgICBjb21taXRDb3VudDogcm93LmNvbW1pdENvdW50ID8/IG51bGwsXG4gICAgICBjb21taXRTaGE6IHJvdy5jb21taXRTaGEgPz8gbnVsbCxcbiAgICAgIGxlYWRUaW1lR2FwOiBsZWFkVGltZUdhcE9mKHJvdyksXG4gICAgfV07XG4gIH0pO1xuICBpZiAodmFsdWVzLmxlbmd0aCA9PT0gMCkgcmV0dXJuIDA7XG5cbiAgdHJ5IHtcbiAgICBjb25zdCBsYW5kZWQgPSBhd2FpdCB3aXRoVGVuYW50VHgoKHR4KSA9PiB0eC5pbnNlcnQoc2NoZW1hLmRvcmFEZXBsb3ltZW50KS52YWx1ZXModmFsdWVzKVxuICAgICAgLm9uQ29uZmxpY3REb1VwZGF0ZSh7XG4gICAgICAgIHRhcmdldDogW3NjaGVtYS5kb3JhRGVwbG95bWVudC5leGVjdXRpb25JZCwgc2NoZW1hLmRvcmFEZXBsb3ltZW50LmVudmlyb25tZW50XSxcbiAgICAgICAgc2V0OiB7XG4gICAgICAgICAgc3VjY2VlZGVkOiBzcWxgZXhjbHVkZWQuc3VjY2VlZGVkYCxcbiAgICAgICAgICBkZXBsb3llZEF0OiBzcWxgZXhjbHVkZWQuZGVwbG95ZWRfYXRgLFxuICAgICAgICAgIHBpcGVsaW5lSWQ6IHNxbGBDT0FMRVNDRShleGNsdWRlZC5waXBlbGluZV9pZCwgJHtzY2hlbWEuZG9yYURlcGxveW1lbnQucGlwZWxpbmVJZH0pYCxcbiAgICAgICAgICAvLyBBIGxhdGVyIGV2ZW50IG1heSByZXNvbHZlIGEgY29tbWl0IHJhbmdlIHRoZSBmaXJzdCBvbmUgY291bGQgbm90LCBzb1xuICAgICAgICAgIC8vIHRoZXNlIGZpbGwgaW4g4oCUIGJ1dCBuZXZlciBibGFuayBvdXQgd2hhdCB3YXMgYWxyZWFkeSBtZWFzdXJlZC5cbiAgICAgICAgICBlYXJsaWVzdENvbW1pdEF0OiBzcWxgQ09BTEVTQ0UoZXhjbHVkZWQuZWFybGllc3RfY29tbWl0X2F0LCAke3NjaGVtYS5kb3JhRGVwbG95bWVudC5lYXJsaWVzdENvbW1pdEF0fSlgLFxuICAgICAgICAgIGxlYWRUaW1lU2Vjb25kczogc3FsYENPQUxFU0NFKGV4Y2x1ZGVkLmxlYWRfdGltZV9zZWNvbmRzLCAke3NjaGVtYS5kb3JhRGVwbG95bWVudC5sZWFkVGltZVNlY29uZHN9KWAsXG4gICAgICAgICAgY29tbWl0Q291bnQ6IHNxbGBDT0FMRVNDRShleGNsdWRlZC5jb21taXRfY291bnQsICR7c2NoZW1hLmRvcmFEZXBsb3ltZW50LmNvbW1pdENvdW50fSlgLFxuICAgICAgICAgIGNvbW1pdFNoYTogc3FsYENPQUxFU0NFKGV4Y2x1ZGVkLmNvbW1pdF9zaGEsICR7c2NoZW1hLmRvcmFEZXBsb3ltZW50LmNvbW1pdFNoYX0pYCxcbiAgICAgICAgICAvLyBUaGUgZ2FwIGlzIGNsZWFyZWQgb25jZSBhIGNvbW1pdCB0aW1lc3RhbXAgYXJyaXZlczogYSByZXNvbHZlZCBsZWFkXG4gICAgICAgICAgLy8gdGltZSBhbmQgYSByZWFzb24gaXQgY291bGQgbm90IGJlIHJlc29sdmVkIG11c3QgbmV2ZXIgY29leGlzdC5cbiAgICAgICAgICBsZWFkVGltZUdhcDogc3FsYENBU0UgV0hFTiBDT0FMRVNDRShleGNsdWRlZC5lYXJsaWVzdF9jb21taXRfYXQsICR7c2NoZW1hLmRvcmFEZXBsb3ltZW50LmVhcmxpZXN0Q29tbWl0QXR9KSBJUyBOT1QgTlVMTFxuICAgICAgICAgICAgVEhFTiBOVUxMIEVMU0UgZXhjbHVkZWQubGVhZF90aW1lX2dhcCBFTkRgLFxuICAgICAgICAgIHVwZGF0ZWRBdDogbmV3IERhdGUoKSxcbiAgICAgICAgfSxcbiAgICAgIH0pXG4gICAgICAucmV0dXJuaW5nKHsgZXhlY3V0aW9uSWQ6IHNjaGVtYS5kb3JhRGVwbG95bWVudC5leGVjdXRpb25JZCB9KSk7XG4gICAgcmV0dXJuIChsYW5kZWQgYXMgdW5rbm93bltdKS5sZW5ndGg7XG4gIH0gY2F0Y2ggKGVycikge1xuICAgIGxvZ2dlci53YXJuKCdEZXBsb3kgcm9sbHVwIHVwc2VydCBmYWlsZWQ7IHRoZSBldmVudHMgbGFuZGVkIGFuZCBhIHJlYnVpbGQgY2FuIHJlY292ZXIgaXQnLCB7XG4gICAgICBkZXBsb3lzOiB2YWx1ZXMubGVuZ3RoLFxuICAgICAgZXJyb3I6IGVycm9yTWVzc2FnZShlcnIpLFxuICAgIH0pO1xuICAgIHJldHVybiAwO1xuICB9XG59XG5cbi8qKlxuICogUmVjb3JkIGEgcG9zdC1kZXBsb3kgb3V0Y29tZSBhZ2FpbnN0IHRoZSBkZXBsb3kgaXQgY29uY2VybnMuXG4gKlxuICogRGVub3JtYWxpemVkIG9udG8gYGRvcmFfZGVwbG95bWVudHNgIHNvIGNoYW5nZS1mYWlsdXJlIHJhdGUgYW5kXG4gKiB0aW1lLXRvLXJlc3RvcmUgYXJlIG9uZSBzY2FuIG92ZXIgdGhlIHdpbmRvdyBpbnN0ZWFkIG9mIGEgam9pbiBwZXIgcmVhZC4gVGhlXG4gKiBgZGVwbG95bWVudF9vdXRjb21lc2AgdGFibGUgc3RheXMgdGhlIGF1dGhvcml0eSDigJQgdGhpcyBpcyB0aGUgY29weSB0aGVcbiAqIGFnZ3JlZ2F0ZXMgdXNlLCBhbmQgaXQgaXMgcmVidWlsZGFibGUgZnJvbSBpdC5cbiAqL1xuZXhwb3J0IGFzeW5jIGZ1bmN0aW9uIGFwcGx5RGVwbG95T3V0Y29tZShcbiAgb3JnSWQ6IHN0cmluZyxcbiAgZXhlY3V0aW9uSWQ6IHN0cmluZyxcbiAgb3V0Y29tZTogJ2ZhaWxlZCcgfCAncmVzdG9yZWQnLFxuICBhdDogRGF0ZSxcbiAgZW52aXJvbm1lbnQ/OiBzdHJpbmcsXG4pOiBQcm9taXNlPHZvaWQ+IHtcbiAgY29uc3Qgd2hlcmUgPSBlbnZpcm9ubWVudFxuICAgID8gYW5kKFxuICAgICAgZXEoc2NoZW1hLmRvcmFEZXBsb3ltZW50LmV4ZWN1dGlvbklkLCBleGVjdXRpb25JZCksXG4gICAgICBlcShzY2hlbWEuZG9yYURlcGxveW1lbnQuZW52aXJvbm1lbnQsIGVudmlyb25tZW50KSxcbiAgICAgIGVxKHNjaGVtYS5kb3JhRGVwbG95bWVudC5vcmdJZCwgb3JnSWQpLFxuICAgIClcbiAgICA6IGFuZChcbiAgICAgIGVxKHNjaGVtYS5kb3JhRGVwbG95bWVudC5leGVjdXRpb25JZCwgZXhlY3V0aW9uSWQpLFxuICAgICAgZXEoc2NoZW1hLmRvcmFEZXBsb3ltZW50Lm9yZ0lkLCBvcmdJZCksXG4gICAgKTtcbiAgLy8gQSBgcmVzdG9yZWRgIG1hcmtlciBvbmx5IG1lYW5zIHNvbWV0aGluZyBmb3IgYSBkZXBsb3kgdGhhdCB3YXMgbWFya2VkIGZhaWxlZDtcbiAgLy8gcmVjb3JkaW5nIGEgcmVzdG9yZSB3aXRoIG5vIGZhaWx1cmUgd291bGQgcHJvZHVjZSBhIG5lZ2F0aXZlIHRpbWUtdG8tcmVzdG9yZS5cbiAgY29uc3Qgc2V0ID0gb3V0Y29tZSA9PT0gJ2ZhaWxlZCdcbiAgICA/IHsgZmFpbGVkQXQ6IGF0LCB1cGRhdGVkQXQ6IG5ldyBEYXRlKCkgfVxuICAgIDogeyByZXN0b3JlZEF0OiBhdCwgdXBkYXRlZEF0OiBuZXcgRGF0ZSgpIH07XG4gIGF3YWl0IHdpdGhUZW5hbnRUeCgodHgpID0+IHR4LnVwZGF0ZShzY2hlbWEuZG9yYURlcGxveW1lbnQpLnNldChzZXQpXG4gICAgLndoZXJlKG91dGNvbWUgPT09ICdyZXN0b3JlZCdcbiAgICAgID8gYW5kKHdoZXJlLCBzcWxgJHtzY2hlbWEuZG9yYURlcGxveW1lbnQuZmFpbGVkQXR9IElTIE5PVCBOVUxMYClcbiAgICAgIDogd2hlcmUpKTtcbn1cblxuLyoqXG4gKiBBdHRyaWJ1dGUgYW4gaW5jaWRlbnQgdG8gdGhlIG1vc3QgcmVjZW50IHN1Y2Nlc3NmdWwgZGVwbG95IHRvIGl0cyBlbnZpcm9ubWVudFxuICogd2l0aGluIHRoZSBjb3JyZWxhdGlvbiB3aW5kb3csIGFuZCBsYWJlbCBob3cgY29uZmlkZW50IHRoYXQgbGluayBpcy5cbiAqXG4gKiBUaGUgY29uZmlkZW5jZSBpcyB0aGUgcG9pbnQuIFwiVGhpcyBkZXBsb3kgYnJva2UgcHJvZHVjdGlvblwiIGFuZCBcImFuIGluY2lkZW50XG4gKiBvcGVuZWQgZWxldmVuIGhvdXJzIGFmdGVyIHRoaXMgZGVwbG95LCBhbmQgdGhlIHdpbmRvdyBoYXBwZW5zIHRvIGJlIHR3ZWx2ZVwiIGFyZVxuICogZGlmZmVyZW50IGNsYWltcywgYW5kIGEgbWFuYWdlciBhY3RzIGRpZmZlcmVudGx5IG9uIGVhY2gg4oCUIHNvIHRoZSByZXBvcnQgc2hvd3NcbiAqIHdoaWNoIG9uZSBpdCBpcyByYXRoZXIgdGhhbiBwcmVzZW50aW5nIGJvdGggYXMgYSBjaGFuZ2UgZmFpbHVyZS5cbiAqL1xuZXhwb3J0IGFzeW5jIGZ1bmN0aW9uIGNvcnJlbGF0ZUluY2lkZW50KFxuICBvcmdJZDogc3RyaW5nLFxuICBlbnZpcm9ubWVudDogc3RyaW5nLFxuICBvcGVuZWRBdDogRGF0ZSxcbiAgd2luZG93SG91cnM6IG51bWJlcixcbik6IFByb21pc2U8eyBleGVjdXRpb25JZDogc3RyaW5nOyBjb25maWRlbmNlOiAnaGlnaCcgfCAnbWVkaXVtJyB8ICdsb3cnIH0gfCBudWxsPiB7XG4gIGNvbnN0IHdpbmRvd1N0YXJ0ID0gbmV3IERhdGUob3BlbmVkQXQuZ2V0VGltZSgpIC0gd2luZG93SG91cnMgKiAzNjAwXzAwMCk7XG4gIGNvbnN0IHJvd3MgPSBhd2FpdCB3aXRoVGVuYW50VHgoKHR4KSA9PiB0eC5zZWxlY3Qoe1xuICAgIGV4ZWN1dGlvbklkOiBzY2hlbWEuZG9yYURlcGxveW1lbnQuZXhlY3V0aW9uSWQsXG4gICAgZGVwbG95ZWRBdDogc2NoZW1hLmRvcmFEZXBsb3ltZW50LmRlcGxveWVkQXQsXG4gIH0pLmZyb20oc2NoZW1hLmRvcmFEZXBsb3ltZW50KVxuICAgIC53aGVyZShhbmQoXG4gICAgICBlcShzY2hlbWEuZG9yYURlcGxveW1lbnQub3JnSWQsIG9yZ0lkKSxcbiAgICAgIGVxKHNjaGVtYS5kb3JhRGVwbG95bWVudC5lbnZpcm9ubWVudCwgZW52aXJvbm1lbnQpLFxuICAgICAgZXEoc2NoZW1hLmRvcmFEZXBsb3ltZW50LnN1Y2NlZWRlZCwgdHJ1ZSksXG4gICAgICBzcWxgJHtzY2hlbWEuZG9yYURlcGxveW1lbnQuZGVwbG95ZWRBdH0gPD0gJHtvcGVuZWRBdH1gLFxuICAgICAgc3FsYCR7c2NoZW1hLmRvcmFEZXBsb3ltZW50LmRlcGxveWVkQXR9ID49ICR7d2luZG93U3RhcnR9YCxcbiAgICApKVxuICAgIC5vcmRlckJ5KHNxbGAke3NjaGVtYS5kb3JhRGVwbG95bWVudC5kZXBsb3llZEF0fSBERVNDYClcbiAgICAubGltaXQoMSkpO1xuICBjb25zdCBkZXBsb3kgPSAocm93cyBhcyBBcnJheTx7IGV4ZWN1dGlvbklkOiBzdHJpbmc7IGRlcGxveWVkQXQ6IERhdGUgfT4pWzBdO1xuICBpZiAoIWRlcGxveSkgcmV0dXJuIG51bGw7XG5cbiAgLy8gQ29uZmlkZW5jZSBieSBob3cgZWFybHkgaW4gdGhlIHdpbmRvdyBpdCBmZWxsLiBUaGUgdGhpcmRzIGFyZSBhIGp1ZGdlbWVudCxcbiAgLy8gYW5kIGEgc3RhdGVkIG9uZTogYW4gaW5jaWRlbnQgd2l0aGluIHRoZSBmaXJzdCB0aGlyZCBvZiB0aGUgd2luZG93IGFmdGVyIGFcbiAgLy8gZGVwbG95IGlzIHRoZSBjYXNlIHRoZSBET1JBIGRlZmluaXRpb24gaXMgYWN0dWFsbHkgYWJvdXQuXG4gIGNvbnN0IGVsYXBzZWQgPSBvcGVuZWRBdC5nZXRUaW1lKCkgLSBkZXBsb3kuZGVwbG95ZWRBdC5nZXRUaW1lKCk7XG4gIGNvbnN0IHdpbmRvd01zID0gd2luZG93SG91cnMgKiAzNjAwXzAwMDtcbiAgY29uc3QgY29uZmlkZW5jZSA9IGVsYXBzZWQgPD0gd2luZG93TXMgLyAzID8gJ2hpZ2gnIDogZWxhcHNlZCA8PSAod2luZG93TXMgKiAyKSAvIDMgPyAnbWVkaXVtJyA6ICdsb3cnO1xuXG4gIGF3YWl0IHdpdGhUZW5hbnRUeCgodHgpID0+IHR4LnVwZGF0ZShzY2hlbWEuZG9yYURlcGxveW1lbnQpXG4gICAgLnNldCh7IGZhaWxlZEF0OiBvcGVuZWRBdCwgY29ycmVsYXRpb25Db25maWRlbmNlOiBjb25maWRlbmNlLCB1cGRhdGVkQXQ6IG5ldyBEYXRlKCkgfSlcbiAgICAud2hlcmUoYW5kKFxuICAgICAgZXEoc2NoZW1hLmRvcmFEZXBsb3ltZW50LmV4ZWN1dGlvbklkLCBkZXBsb3kuZXhlY3V0aW9uSWQpLFxuICAgICAgZXEoc2NoZW1hLmRvcmFEZXBsb3ltZW50LmVudmlyb25tZW50LCBlbnZpcm9ubWVudCksXG4gICAgICAvLyBEbyBub3Qgb3ZlcndyaXRlIGEgZmFpbHVyZSBhbHJlYWR5IGF0dHJpYnV0ZWQ6IHRoZSBGSVJTVCBpbmNpZGVudCBsaW5rZWRcbiAgICAgIC8vIHRvIGEgZGVwbG95IGlzIHRoZSBvbmUgdGhhdCBtYWRlIGl0IGEgY2hhbmdlIGZhaWx1cmUsIGFuZCBhIGxhdGVyIG9uZVxuICAgICAgLy8gcmUtZGF0aW5nIGl0IHdvdWxkIG1vdmUgdGltZS10by1yZXN0b3JlLlxuICAgICAgaXNOdWxsKHNjaGVtYS5kb3JhRGVwbG95bWVudC5mYWlsZWRBdCksXG4gICAgKSkpO1xuICByZXR1cm4geyBleGVjdXRpb25JZDogZGVwbG95LmV4ZWN1dGlvbklkLCBjb25maWRlbmNlIH07XG59XG4iXX0=
|
|
@@ -64,6 +64,20 @@ export declare function round(value: number, digits: number): number;
|
|
|
64
64
|
* fixture tests able to prove the metric math without a live Postgres).
|
|
65
65
|
*/
|
|
66
66
|
export declare function median(values: number[]): number | null;
|
|
67
|
+
/**
|
|
68
|
+
* The `p`-th percentile of a sample (0 < p < 100), or `null` when empty.
|
|
69
|
+
*
|
|
70
|
+
* Linear interpolation between order statistics — the same definition as
|
|
71
|
+
* Postgres's `PERCENTILE_CONT`, so a reader comparing this to a hand-written SQL
|
|
72
|
+
* query gets the same number. {@link median} is p50 by this definition too; it
|
|
73
|
+
* stays a separate function because it is the level-scoring input and is read in
|
|
74
|
+
* far more places.
|
|
75
|
+
*
|
|
76
|
+
* Tails matter here: a median lead time of 2 hours with a p95 of 3 weeks is a
|
|
77
|
+
* team with a fast path and a stuck queue, and the median alone hides that
|
|
78
|
+
* entirely. That is why the plan asks for p90/p95 rather than more levels.
|
|
79
|
+
*/
|
|
80
|
+
export declare function percentile(values: number[], p: number): number | null;
|
|
67
81
|
/**
|
|
68
82
|
* Roll a group of deploy/stage events (grouped per execution) up to one terminal
|
|
69
83
|
* status: FAILED wins, then SUCCEEDED, else OTHER (still-running / non-terminal).
|