| /** |
| * @module module/alert-config-sk |
| * @description <h2><code>alert-config-sk</code></h2> |
| * |
| * Control that allows editing an alert.Config. |
| * |
| */ |
| import { define } from 'elements-sk/define'; |
| import { html } from 'lit-html'; |
| |
| import 'elements-sk/checkbox-sk'; |
| import 'elements-sk/multi-select-sk'; |
| import 'elements-sk/select-sk'; |
| import 'elements-sk/spinner-sk'; |
| import 'elements-sk/styles/buttons'; |
| import { errorMessage } from 'elements-sk/errorMessage'; |
| import { jsonOrThrow } from 'common-sk/modules/jsonOrThrow'; |
| import { SpinnerSk } from 'elements-sk/spinner-sk/spinner-sk'; |
| import { SelectSkSelectionChangedEventDetail } from 'elements-sk/select-sk/select-sk'; |
| import { MultiSelectSkSelectionChangedEventDetail } from 'elements-sk/multi-select-sk/multi-select-sk'; |
| import { ElementSk } from '../../../infra-sk/modules/ElementSk'; |
| import { |
| ParamSet, |
| Alert, |
| Direction, |
| StepDetection, |
| ConfigState, |
| TryBugRequest, |
| TryBugResponse, |
| } from '../json'; |
| import { QuerySkQueryChangeEventDetail } from '../../../infra-sk/modules/query-sk/query-sk'; |
| import { AlgoSelectAlgoChangeEventDetail } from '../algo-select-sk/algo-select-sk'; |
| |
| import '../algo-select-sk'; |
| import '../query-chooser-sk'; |
| |
| const toDirection = (val: string | null): Direction => { |
| if (val === 'UP') { |
| return 'UP'; |
| } if (val === 'DOWN') { |
| return 'DOWN'; |
| } |
| return 'BOTH'; |
| }; |
| |
| const stepDetections: StepDetection[] = ['', 'absolute', 'percent', 'cohen']; |
| const toStepDetection = (s: string | null): StepDetection => { |
| if (s === null) { |
| return ''; |
| } |
| if (stepDetections.indexOf(s as StepDetection) !== -1) { |
| return s as StepDetection; |
| } |
| return ''; |
| }; |
| |
| const toConfigState = (s: string | null): ConfigState => { |
| if (s === 'ACTIVE') { |
| return 'ACTIVE'; |
| } |
| return 'DELETED'; |
| }; |
| |
| export class AlertConfigSk extends ElementSk { |
| private _paramset: ParamSet = {}; |
| |
| private _config: Alert; |
| |
| private _key_order: string[] | null = []; |
| |
| private paramkeys: string[] = []; |
| |
| private bugSpinner: SpinnerSk | null = null; |
| |
| private alertSpinner: SpinnerSk | null = null; |
| |
| constructor() { |
| super(AlertConfigSk.template); |
| this._paramset = {}; |
| this.paramkeys = []; |
| this._config = { |
| id_as_string: '-1', |
| display_name: 'Name', |
| query: '', |
| alert: '', |
| interesting: 0, |
| bug_uri_template: '', |
| algo: 'kmeans', |
| state: 'ACTIVE', |
| owner: '', |
| step_up_only: false, |
| direction: 'BOTH', |
| radius: 10, |
| k: 50, |
| group_by: '', |
| sparse: false, |
| minimum_num: 0, |
| category: 'Experimental', |
| step: '', |
| }; |
| } |
| |
| private static template = (ele: AlertConfigSk) => html` |
| <h3>Display Name</h3> |
| <label for="display-name">Display Name</label> |
| <input |
| id="display-name" |
| type="text" |
| .value=${ele._config.display_name} |
| @change=${(e: InputEvent) => (ele._config.display_name = (e.target! as HTMLInputElement).value)} |
| /> |
| |
| <h3>Category</h3> |
| <label for="category">Alerts will be grouped by category.</label> |
| <input |
| id="category" |
| type="text" |
| .value=${ele._config.category} |
| @input=${(e: InputEvent) => (ele._config.category = (e.target! as HTMLInputElement).value)} |
| /> |
| |
| <h3>Which traces should be monitored</h3> |
| <query-chooser-sk |
| id="querychooser" |
| .paramset=${ele.paramset} |
| .key_order=${ele.key_order} |
| current_query=${ele._config.query} |
| count_url="/_/count/" |
| @query-change=${(e: CustomEvent<QuerySkQueryChangeEventDetail>) => (ele._config.query = e.detail.q)} |
| ></query-chooser-sk> |
| |
| <div> |
| <a |
| href="/e/?queries=${encodeURIComponent(ele._config.query)}" |
| target="_blank" |
| > |
| Preview traces that match the query |
| </a> |
| </div> |
| |
| <h3>What triggers an alert</h3> |
| <h4>Grouping</h4> |
| <label for="grouping"> |
| Are the traces k-means clustered and Step Detection done on the centroid, |
| or is Step Detection done on each trace individually. |
| </label> |
| <algo-select-sk |
| id="grouping" |
| algo=${ele._config.algo} |
| @algo-change=${(e: CustomEvent<AlgoSelectAlgoChangeEventDetail>) => (ele._config.algo = e.detail.algo)} |
| ></algo-select-sk> |
| |
| <h4>Step Detection</h4> |
| <label for="step"> |
| Choose the algorithm used to determine if a regression has occurred. The |
| actual value is set in |
| <em>Threshold</em> |
| . |
| </label> |
| <select-sk |
| id="step" |
| @selection-changed=${( |
| e: CustomEvent<SelectSkSelectionChangedEventDetail>, |
| ) => (ele._config.step = toStepDetection( |
| (e.target! as HTMLDivElement).children[ |
| e.detail.selection |
| ].getAttribute('value'), |
| ))} |
| > |
| <div value="" ?selected=${ele._config.step === ''}> |
| Regression = Step Size/Variance. This is the original regression factor. |
| </div> |
| <div value="absolute" ?selected=${ele._config.step === 'absolute'}> |
| A change in absolute magnitude. Threshold is the minimum difference to |
| trigger an alert. |
| </div> |
| <div value="percent" ?selected=${ele._config.step === 'percent'}> |
| A change by percent. Threshold is a value in [0.0, 1.0] and the minimum |
| difference to trigger an alert. |
| </div> |
| <div value="cohen" ?selected=${ele._config.step === 'cohen'}> |
| Use Cohen's d method to detect a change. Threshold is the standard |
| deviations that the mean must move to trigger an alert. |
| </div> |
| </select-sk> |
| <h4>Threshold</h4> |
| <label for="threshold"> |
| The threshold for Step Detection to trigger an alert. The meaning of the |
| value and meaningful range depends on the algorithm chosen for |
| <em>Step Detection</em> |
| . |
| </label> |
| <input |
| id="threshold" |
| type="number" |
| min="1" |
| max="500" |
| .value=${ele._config.interesting} |
| @input=${(e: InputEvent) => (ele._config.interesting = +(e.target! as HTMLInputElement).value)} |
| /> |
| |
| <h4>K</h4> |
| <label for="k"> |
| The number of clusters. Only used when Grouping is K-Means. 0 = use a |
| server chosen value. |
| </label> |
| <input |
| id="k" |
| type="number" |
| min="0" |
| .value=${ele._config.k} |
| @input=${(e: InputEvent) => (ele._config.k = +(e.target! as HTMLInputElement).value)} |
| /> |
| |
| <h4>Radius</h4> |
| <label for="radius"> |
| Number of commits on either side to consider. 0 = use a server chosen |
| value. |
| </label> |
| <input |
| id="radius" |
| type="number" |
| min="0" |
| .value=${ele._config.radius} |
| @input=${(e: InputEvent) => (ele._config.radius = +(e.target! as HTMLInputElement).value)} |
| /> |
| |
| <h4>Step Direction</h4> |
| <select-sk |
| @selection-changed=${( |
| e: CustomEvent<SelectSkSelectionChangedEventDetail>, |
| ) => (ele._config.direction = toDirection( |
| (e.target! as HTMLDivElement).children[ |
| e.detail.selection |
| ].getAttribute('value'), |
| ))} |
| > |
| <div value="BOTH" ?selected=${ele._config.direction === 'BOTH'}> |
| Either step up or step down trigger an alert. |
| </div> |
| <div value="UP" ?selected=${ele._config.direction === 'UP'}> |
| Step up triggers an alert. |
| </div> |
| <div value="DOWN" ?selected=${ele._config.direction === 'DOWN'}> |
| Step down triggers an alert. |
| </div> |
| </select-sk> |
| |
| <h4>Minimum</h4> |
| <label for="min"> |
| Minimum number of interesting traces to trigger an alert. |
| </label> |
| <input |
| id="min" |
| type="number" |
| .value=${ele._config.minimum_num} |
| @input=${(e: InputEvent) => (ele._config.minimum_num = +(e.target! as HTMLInputElement).value)} |
| /> |
| |
| <h4>Sparse</h4> |
| <checkbox-sk |
| ?checked=${ele._config.sparse} |
| @input=${(e: InputEvent) => (ele._config.sparse = (e.target! as HTMLInputElement).checked)} |
| label="Data is sparse, so only include commits that have data." |
| ></checkbox-sk> |
| |
| <h3>Where are alerts sent</h3> |
| <label for="sent"> |
| Alert Destination: Comma separated list of email addresses. |
| </label> |
| <input |
| id="sent" |
| .value=${ele._config.alert} |
| @input=${(e: InputEvent) => (ele._config.alert = (e.target! as HTMLInputElement).value)} |
| /> |
| <button @click=${ele.testAlert}>Test</button> |
| <spinner-sk id="alertSpinner"></spinner-sk> |
| |
| <h3>Where are bugs filed</h3> |
| <label for="template"> |
| Bug URI Template: {cluster_url}, {commit_url}, and {message}. |
| </label> |
| <input |
| id="template" |
| .value=${ele._config.bug_uri_template} |
| @input=${(e: InputEvent) => (ele._config.bug_uri_template = (e.target! as HTMLInputElement).value)} |
| /> |
| <button @click=${ele.testBugTemplate}>Test</button> |
| <spinner-sk id="bugSpinner"></spinner-sk> |
| |
| <h3>Who owns this alert</h3> |
| <label for="owner">Email address of owner.</label> |
| <input |
| id="owner" |
| .value=${ele._config.owner} |
| @input=${(e: InputEvent) => (ele._config.owner = (e.target! as HTMLInputElement).value)} |
| /> |
| |
| <h3>Group By</h3> |
| <label for="groupby"> |
| Group clusters by these parameters. (Multiselect) |
| </label> |
| <multi-select-sk |
| @selection-changed=${( |
| e: CustomEvent<MultiSelectSkSelectionChangedEventDetail>, |
| ) => (ele._config.group_by = e.detail.selection |
| .map((i) => ele.paramkeys[i]) |
| .join(','))} |
| id="groupby" |
| > |
| ${AlertConfigSk._groupByChoices(ele)} |
| </multi-select-sk> |
| |
| <h3>Status</h3> |
| <select-sk |
| .selection=${ele._config.state === 'ACTIVE' ? 0 : 1} |
| @selection-changed=${( |
| e: CustomEvent<SelectSkSelectionChangedEventDetail>, |
| ) => (ele._config.state = toConfigState( |
| (e.target! as HTMLDivElement).children[ |
| e.detail.selection |
| ].getAttribute('value'), |
| ))} |
| > |
| <div |
| value="ACTIVE" |
| title="Clusters that match this will generate alerts." |
| > |
| Active |
| </div> |
| <div value="DELETED" title="Currently inactive.">Deleted</div> |
| </select-sk> |
| `; |
| |
| private static _groupByChoices = (ele: AlertConfigSk) => { |
| const groups = ele._config.group_by.split(','); |
| return ele.paramkeys.map( |
| (p) => html`<div ?selected=${groups.indexOf(p) !== -1}>${p}</div>`, |
| ); |
| }; |
| |
| |
| connectedCallback(): void { |
| super.connectedCallback(); |
| this._upgradeProperty('config'); |
| this._upgradeProperty('paramset'); |
| if (window.sk && window.sk.perf && window.sk.perf.key_order) { |
| this._key_order = window.sk.perf.key_order; |
| } |
| this._render(); |
| this.bugSpinner = this.querySelector('#bugSpinner'); |
| this.alertSpinner = this.querySelector('#alertSpinner'); |
| } |
| |
| private testBugTemplate() { |
| this.bugSpinner!.active = true; |
| const body: TryBugRequest = { |
| bug_uri_template: this.config.bug_uri_template, |
| }; |
| fetch('/_/alert/bug/try', { |
| method: 'POST', |
| body: JSON.stringify(body), |
| headers: { |
| 'Content-Type': 'application/json', |
| }, |
| }) |
| .then(jsonOrThrow) |
| .then((json: TryBugResponse) => { |
| this.bugSpinner!.active = false; |
| if (json.url) { |
| // Open the bug reporting page in a new window. |
| window.open(json.url, '_blank'); |
| } |
| }) |
| .catch((msg) => { |
| this.bugSpinner!.active = false; |
| errorMessage(msg); |
| }); |
| } |
| |
| private testAlert() { |
| this.alertSpinner!.active = true; |
| const body: Alert = this.config; |
| fetch('/_/alert/notify/try', { |
| method: 'POST', |
| body: JSON.stringify(body), |
| headers: { |
| 'Content-Type': 'application/json', |
| }, |
| }) |
| .then(() => { |
| this.alertSpinner!.active = false; |
| }) |
| .catch((msg) => { |
| this.alertSpinner!.active = false; |
| errorMessage(msg); |
| }); |
| } |
| |
| /** @prop paramset {string} A serialized paramtools.ParamSet. */ |
| get paramset(): ParamSet { |
| return this._paramset; |
| } |
| |
| set paramset(val: ParamSet) { |
| if (val === undefined) { |
| return; |
| } |
| this._paramset = val; |
| this.paramkeys = Object.keys(val); |
| this.paramkeys.sort(); |
| this._render(); |
| } |
| |
| /** @prop config {Object} A serialized alerts.Alert. */ |
| get config(): Alert { |
| return this._config; |
| } |
| |
| set config(val: Alert) { |
| if (!val || Object.keys(val).length === 0) { |
| return; |
| } |
| this._config = val; |
| if (this._config.interesting === 0) { |
| this._config.interesting = window.sk.perf.interesting; |
| } |
| if (this._config.radius === 0) { |
| this._config.radius = window.sk.perf.radius; |
| } |
| this._render(); |
| } |
| |
| /** @prop key_order {string} The order of keys, passed to query-sk. */ |
| get key_order(): string[] | null { |
| return this._key_order; |
| } |
| |
| set key_order(val: string[] | null) { |
| if (val === undefined) { |
| return; |
| } |
| this._key_order = val; |
| this._render(); |
| } |
| } |
| |
| define('alert-config-sk', AlertConfigSk); |