Skip to content

Commit f29f991

Browse files
author
Nicholas Smith
committed
Add support for determining error priority
1 parent 19580ee commit f29f991

File tree

7 files changed

+166
-41
lines changed

7 files changed

+166
-41
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "error-sync-lib",
3-
"version": "0.1.1",
3+
"version": "0.1.2",
44
"private": "true",
55
"scripts": {
66
"build": "yarn build:esm && yarn build:cjs",

src/Synchronizer.ts

Lines changed: 100 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
import { Alert, AlertContent, CacheName, Error, ErrorGroup, ErrorPriority, Ticket, TicketContent } from './models';
2-
import { AlertProviderInterface, CacheProviderInterface, ErrorProviderInterface, TicketProviderInterface } from './interfaces';
2+
import {
3+
AlertProviderInterface,
4+
CacheProviderInterface,
5+
ErrorProviderInterface,
6+
PrioritizationProviderInterface,
7+
TicketProviderInterface
8+
} from './interfaces';
9+
import { ErrorCountPrioritizationProvider } from "./providers";
310

411
const crypto = require('crypto');
512

@@ -14,9 +21,16 @@ export type SynchronizerResult = {
1421
exitCode: number,
1522
}
1623

24+
export type SynchronizerErrorProviderConfig = {
25+
name: string,
26+
provider: ErrorProviderInterface,
27+
prioritizationProvider?: PrioritizationProviderInterface,
28+
lookbackHours?: number,
29+
maxErrors?: number,
30+
}
31+
1732
export type SynchronizerConfig = {
18-
serverErrorProvider?: ErrorProviderInterface,
19-
clientErrorProvider?: ErrorProviderInterface,
33+
errors: SynchronizerErrorProviderConfig[],
2034
ticketProvider: TicketProviderInterface,
2135
alertProvider: AlertProviderInterface,
2236
cacheProvider: CacheProviderInterface,
@@ -27,61 +41,112 @@ export class Synchronizer {
2741

2842
public constructor(config: SynchronizerConfig) {
2943
this.config = config;
44+
45+
// validate config
46+
if (this.config.errors.length === 0) {
47+
throw new Error('There must be at least one error provider set in the configuration');
48+
}
49+
50+
// apply defaults for anything which is not set
51+
for (let provider of this.config.errors) {
52+
provider.lookbackHours ??= 24;
53+
provider.maxErrors ??= 1000;
54+
provider.prioritizationProvider ??= new ErrorCountPrioritizationProvider();
55+
}
3056
}
3157

3258
public async run(): Promise<SynchronizerResult> {
33-
let result: SynchronizerResult = {
59+
let finalResult: SynchronizerResult = {
3460
completedErrorGroups: [],
3561
errors: [],
3662
exitCode: 0
3763
};
3864

65+
// run all error provider synchronizations in parallel
3966
try {
40-
const errors = await this.config.serverErrorProvider.getErrors(24, 1000);
41-
const errorGroups: ErrorGroup[] = [];
42-
43-
// build up the error groups from raw errors, which drive all downstream work
44-
errors.forEach((error) => this.addToErrorGroups(error, errorGroups));
45-
46-
// for each error group, create / update a ticket and alert as needed. in most cases, no work
47-
// is done because the ticket + alert has already been created and does not need to be updated.
48-
for (const errorGroup of errorGroups) {
67+
const errorPromises = this.config.errors.map(async (errorConfig) => {
4968
try {
50-
this.syncErrorGroup(errorGroup);
51-
result.completedErrorGroups.push(errorGroup);
69+
this.runForErrorProvider(errorConfig, finalResult);
5270
} catch (e) {
53-
result.errors.push({
71+
finalResult.exitCode = 1;
72+
finalResult.errors.push({
5473
message: e.message || e,
55-
errorGroup,
5674
});
5775

58-
console.error('Failed to synchronize an error into the ticketing and/or alerting system.');
59-
console.error(`The relevant error is named "${errorGroup.name}"`);
60-
console.error('The exception which occurred is:', e);
76+
console.error(e);
77+
}
78+
});
79+
80+
// check for any promise rejections from our error provider synchronizations
81+
const providerResults = await Promise.allSettled(errorPromises);
82+
for (const [index, providerResult] of providerResults.entries()) {
83+
if (providerResult.status === 'rejected') {
84+
const providerName = this.config.errors[index].name;
85+
console.error('An unexpected exception occurred while trying to synchronize errors for the ' +
86+
`provider named "${providerName}":`, providerResult.reason);
87+
finalResult.exitCode = 2;
88+
finalResult.errors.push({
89+
message: providerResult.reason.message || providerResult.reason,
90+
});
6191
}
6292
}
93+
} catch (e) {
94+
finalResult.exitCode = 3;
95+
finalResult.errors.push({
96+
message: e.message || e,
97+
});
98+
99+
console.error('An unexpected exception occurred while running the error synchronizations', e);
100+
}
63101

64-
// persist all cached data changes
102+
// persist all cached data changes
103+
try {
65104
this.config.cacheProvider.saveAllCaches();
66105
} catch (e) {
67-
result.exitCode = 1;
68-
result.errors.push({
106+
finalResult.exitCode = 4;
107+
finalResult.errors.push({
69108
message: e.message || e,
70109
});
71110

72-
console.error(e);
111+
console.error('An unexpected exception occurred while running the error synchronizations', e);
73112
}
74113

75-
if (result.errors.length > 0) {
114+
if (finalResult.errors.length > 0) {
76115
console.error('Some errors were not synchronized to the ticketing and/or alerting system. Please see errors above.');
77-
result.exitCode = 2;
116+
finalResult.exitCode = finalResult.exitCode || 5;
78117
}
79118

80-
return result;
119+
return finalResult;
120+
}
121+
122+
private async runForErrorProvider(errorConfig: SynchronizerErrorProviderConfig, result: SynchronizerResult) {
123+
const errors = await errorConfig.provider.getErrors(errorConfig.lookbackHours, errorConfig.maxErrors);
124+
const errorGroups: ErrorGroup[] = [];
125+
126+
// build up the error groups from raw errors, which drive all downstream work
127+
errors.forEach((error) => this.addToErrorGroups(error, errorGroups, errorConfig.name));
128+
129+
// for each error group, create / update a ticket and alert as needed. in most cases, no work
130+
// is done because the ticket + alert has already been created and does not need to be updated.
131+
for (const errorGroup of errorGroups) {
132+
try {
133+
this.syncErrorGroup(errorGroup, errorConfig);
134+
result.completedErrorGroups.push(errorGroup);
135+
} catch (e) {
136+
result.errors.push({
137+
message: e.message || e,
138+
errorGroup,
139+
});
140+
141+
console.error('Failed to synchronize an error into the ticketing and/or alerting system.');
142+
console.error(`The relevant error is named "${errorGroup.name}" from provider "${errorConfig.name}"`);
143+
console.error('The exception which occurred is:', e);
144+
}
145+
}
81146
}
82147

83-
private async syncErrorGroup(errorGroup: ErrorGroup) {
84-
errorGroup.priority = this.determineErrorPriority(errorGroup);
148+
private async syncErrorGroup(errorGroup: ErrorGroup, errorConfig: SynchronizerErrorProviderConfig) {
149+
errorGroup.priority = await errorConfig.prioritizationProvider.determinePriority(errorGroup);
85150
errorGroup.ticket = await this.config.cacheProvider.getObject(errorGroup.clientId, CacheName.Tickets);
86151
errorGroup.alert = await this.config.cacheProvider.getObject(errorGroup.clientId, CacheName.Alerts);
87152

@@ -129,16 +194,14 @@ export class Synchronizer {
129194
this.config.cacheProvider.setObject(errorGroup.alert.id, errorGroup.alert, CacheName.Alerts, false);
130195
}
131196

132-
private createErrorGroup(error: Error): ErrorGroup {
197+
private createErrorGroup(error: Error, sourceName: string): ErrorGroup {
133198
// truncate the error to the first 500 characters
134199
const maxNameLength = 500;
135-
if (error.name.length > maxNameLength) {
136-
error.name = error.name.substr(0, maxNameLength);
137-
}
200+
error.name = `[${sourceName}] ${error.name}`.substr(0, maxNameLength);
138201

139202
// wipe out line numbers
140203
let normalizedName = error.name;
141-
normalizedName = normalizedName.replace(/\.(php|js|jsx|ts|tsx|py|go|java)[:@]\d+/i, '.$1:XXX');
204+
normalizedName = normalizedName.replace(/\.(js|jsx|ts|tsx|php|py|go|java|cpp|h|c|cs|ex|exs|rb)[:@]\d+/i, '.$1:XXX');
142205

143206
// remove TypeError prefix from client errors that some browsers may emit
144207
normalizedName = normalizedName.replace(/(TypeError:\s*)/i, '');
@@ -148,6 +211,7 @@ export class Synchronizer {
148211

149212
return {
150213
name: normalizedName,
214+
sourceName,
151215
type: error.type,
152216
priority: ErrorPriority.P5,
153217
clientId: hash,
@@ -159,8 +223,8 @@ export class Synchronizer {
159223
};
160224
}
161225

162-
private addToErrorGroups(error: Error, errorGroups: ErrorGroup[]) {
163-
const newErrorGroup = this.createErrorGroup(error);
226+
private addToErrorGroups(error: Error, errorGroups: ErrorGroup[], sourceName: string) {
227+
const newErrorGroup = this.createErrorGroup(error, sourceName);
164228

165229
for (let i = 0; i < errorGroups.length; ++i) {
166230
const existingErrorGroup = errorGroups[i];
@@ -200,8 +264,4 @@ export class Synchronizer {
200264
existingAlert.priority !== freshAlertContent.priority ||
201265
existingAlert.ticketUrl !== freshAlertContent.ticketUrl;
202266
}
203-
204-
private determineErrorPriority(errorGroup: ErrorGroup): ErrorPriority {
205-
return ErrorPriority.P5; // TODO
206-
}
207267
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { ErrorGroup, ErrorPriority } from '../models';
2+
3+
export interface PrioritizationProviderInterface {
4+
determinePriority(errorGroup: ErrorGroup): Promise<ErrorPriority>;
5+
}

src/interfaces/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from './AlertProviderInterface';
22
export * from './CacheProviderInterface';
33
export * from './ErrorProviderInterface';
4+
export * from './PrioritizationProviderInterface';
45
export * from './TicketProviderInterface';

src/models/Error.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export type Error = {
2929

3030
export type ErrorGroup = {
3131
name: string,
32+
sourceName: string,
3233
type: ErrorType,
3334
priority: string,
3435
clientId: string,
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { ErrorGroup, ErrorPriority } from '../models';
2+
import { PrioritizationProviderInterface } from '../interfaces';
3+
4+
export type ErrorCountPrioritizationProviderThreshold = {
5+
threshold: number,
6+
priority: ErrorPriority,
7+
label: string,
8+
};
9+
10+
export type ErrorCountPrioritizationProviderConfig = {
11+
thresholds: ErrorCountPrioritizationProviderThreshold[],
12+
};
13+
14+
export const DefaultErrorCountPrioritizationProviderConfig = {
15+
thresholds: [{
16+
// affecting zero users
17+
threshold: 1,
18+
priority: ErrorPriority.P5,
19+
label: '0',
20+
}, {
21+
// affecting [1, 10) users
22+
threshold: 10,
23+
priority: ErrorPriority.P4,
24+
label: '>= 1 and < 10',
25+
}, {
26+
// affecting [10, 30) users
27+
threshold: 30,
28+
priority: ErrorPriority.P3,
29+
label: '>= 10 and < 30',
30+
}, {
31+
// affecting [30, 90) users
32+
threshold: 90,
33+
priority: ErrorPriority.P2,
34+
label: '>= 30 and < 90',
35+
}, {
36+
// affecting [90, infinity) users
37+
threshold: Number.MAX_SAFE_INTEGER,
38+
priority: ErrorPriority.P1,
39+
label: '>= 90',
40+
}],
41+
};
42+
43+
export class ErrorCountPrioritizationProvider implements PrioritizationProviderInterface {
44+
private config: ErrorCountPrioritizationProviderConfig;
45+
46+
public constructor(config?: ErrorCountPrioritizationProviderConfig) {
47+
this.config = config ?? DefaultErrorCountPrioritizationProviderConfig;
48+
}
49+
50+
public async determinePriority(errorGroup: ErrorGroup): Promise<ErrorPriority> {
51+
for (const threshold of this.config.thresholds) {
52+
if (errorGroup.count < threshold.threshold) {
53+
return threshold.priority;
54+
}
55+
}
56+
}
57+
}

src/providers/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export * from './ErrorCountPrioritizationProvider';
12
export * from './JiraTicketProvider';
23
export * from './NewRelicServerErrorProvider';
34
export * from './OpsGenieAlertProvider';

0 commit comments

Comments
 (0)