Skip to content

Commit 3002b6d

Browse files
committed
Implemented DuplicateChecker
1 parent 556848f commit 3002b6d

7 files changed

+221
-20
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { ContextData } from '../ContextData';
2+
import { EventPluginContext } from '../EventPluginContext';
3+
import { DuplicateCheckerPlugin } from './DuplicateCheckerPlugin';
4+
import { ErrorPlugin } from './ErrorPlugin';
5+
import { createFixture } from './EventPluginTestFixture';
6+
7+
describe('DuplicateCheckerPlugin', () => {
8+
9+
let target: DuplicateCheckerPlugin;
10+
let now: number;
11+
12+
beforeEach(() => {
13+
target = new DuplicateCheckerPlugin();
14+
(<any>target).getNow = () => now;
15+
now = 0;
16+
});
17+
18+
function run(exception: Error) {
19+
let context: EventPluginContext;
20+
let contextData: ContextData;
21+
({
22+
context,
23+
contextData
24+
} = createFixture());
25+
26+
contextData.setException(exception);
27+
28+
let errorPlugin = new ErrorPlugin();
29+
errorPlugin.run(context);
30+
target.run(context);
31+
32+
return context;
33+
}
34+
35+
it('should ignore duplicate within window', () => {
36+
let exception = createException([{
37+
name: "methodA"
38+
}]);
39+
run(exception);
40+
let contextOfSecondRun = run(exception);
41+
expect(contextOfSecondRun.cancelled).toBeTruthy();
42+
});
43+
44+
it('shouldn\'t ignore error without stack', () => {
45+
let exception = createException();
46+
run(exception);
47+
let contextOfSecondRun = run(exception);
48+
expect(contextOfSecondRun.cancelled).toBeFalsy();
49+
});
50+
51+
it('shouldn\'t ignore different stack within window', () => {
52+
let exception1 = createException([{
53+
name: "methodA"
54+
}]);
55+
run(exception1);
56+
let exception2 = createException([{
57+
name: "methodB"
58+
}]);
59+
let contextOfSecondRun = run(exception2);
60+
expect(contextOfSecondRun.cancelled).toBeFalsy();
61+
});
62+
63+
it('shouldn\'t ignore duplicate after window', () => {
64+
let exception = createException([{
65+
name: "methodA"
66+
}]);
67+
run(exception);
68+
69+
now = 3000;
70+
let contextOfSecondRun = run(exception);
71+
expect(contextOfSecondRun.cancelled).toBeFalsy();
72+
});
73+
});
74+
75+
function createException(stack?) {
76+
try {
77+
throw new Error();
78+
}
79+
catch (e) {
80+
e.testStack = stack;
81+
return e;
82+
}
83+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { IInnerError } from '../../models/IInnerError';
2+
import { IStackFrame } from '../../models/IStackFrame';
3+
4+
import { ILog } from '../../logging/ILog';
5+
6+
import { IEventPlugin } from '../IEventPlugin';
7+
import { EventPluginContext } from '../EventPluginContext';
8+
9+
const ERROR_KEY: string = '@error';
10+
const WINDOW_MILLISECONDS = 2000;
11+
const MAX_QUEUE_LENGTH = 10;
12+
13+
export class DuplicateCheckerPlugin implements IEventPlugin {
14+
public priority: number = 40;
15+
public name: string = 'DuplicateCheckerPlugin';
16+
17+
private recentlyProcessedErrors: TimestampedHash[] = [];
18+
19+
public run(context: EventPluginContext, next?: () => void): void {
20+
if (context.event.type === 'error') {
21+
let error = context.event.data[ERROR_KEY];
22+
let isDuplicate = this.checkDuplicate(error, context.log);
23+
if (isDuplicate) {
24+
context.cancelled = true;
25+
return;
26+
}
27+
}
28+
29+
next && next();
30+
}
31+
32+
private getNow() {
33+
return Date.now();
34+
}
35+
36+
private checkDuplicate(error: IInnerError, log: ILog): boolean {
37+
let now = this.getNow();
38+
let repeatWindow = now - WINDOW_MILLISECONDS;
39+
let hashCode: number;
40+
while (error) {
41+
hashCode = getHashCodeForError(error);
42+
43+
// make sure that we don't process the same error multiple times within the repeat window
44+
if (hashCode && this.recentlyProcessedErrors.some(h =>
45+
h.hash == hashCode && h.timestamp >= repeatWindow)) {
46+
log.info(`Ignoring duplicate error event: hash=${hashCode}`);
47+
return true;
48+
}
49+
50+
// add this exception to our list of recent errors that we have processed
51+
this.recentlyProcessedErrors.push({ hash: hashCode, timestamp: now });
52+
53+
// only keep the last 10 recent errors
54+
while (this.recentlyProcessedErrors.length > MAX_QUEUE_LENGTH) {
55+
this.recentlyProcessedErrors.shift();
56+
}
57+
58+
error = error.inner;
59+
}
60+
61+
return false;
62+
}
63+
}
64+
65+
interface TimestampedHash {
66+
hash: number;
67+
timestamp: number
68+
}
69+
70+
function getHashCodeForError(error: IInnerError): number {
71+
if (!error.stack_trace) {
72+
return null;
73+
}
74+
75+
let stack = JSON.stringify(error.stack_trace);
76+
return getHashCode(stack);
77+
}
78+
79+
function getHashCode(s: string): number {
80+
let hash = 0, length = s.length, char;
81+
for (let i = 0; i < length; i++) {
82+
char = s.charCodeAt(i);
83+
hash = ((hash << 5) - hash) + char;
84+
hash |= 0;
85+
}
86+
return hash;
87+
}

src/plugins/default/EnvironmentInfoPlugin.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { EventPluginContext } from '../EventPluginContext';
33
import { IEnvironmentInfo } from '../../models/IEnvironmentInfo';
44

55
export class EnvironmentInfoPlugin implements IEventPlugin {
6-
public priority:number = 70;
6+
public priority:number = 80;
77
public name:string = 'EnvironmentInfoPlugin';
88

99
public run(context:EventPluginContext, next?:() => void): void {

src/plugins/default/ErrorPlugin-spec.ts

+6-17
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { IErrorParser } from '../../services/IErrorParser';
66

77
import { ErrorPlugin } from './ErrorPlugin';
88
import { CapturedExceptions } from './ErrorPlugin-spec-exceptions';
9-
9+
import { createFixture } from './EventPluginTestFixture';
1010

1111
function BaseTestError() {
1212
this.name = 'NotImplementedError';
@@ -31,22 +31,11 @@ describe('ErrorPlugin', () => {
3131
let event: IEvent;
3232

3333
beforeEach(() => {
34-
errorParser = {
35-
parse: (c: EventPluginContext, exception: Error) => ({
36-
type: exception.name,
37-
message: exception.message
38-
})
39-
};
40-
client = {
41-
config: {
42-
errorParser
43-
}
44-
};
45-
event = {
46-
data: {}
47-
};
48-
contextData = new ContextData();
49-
context = new EventPluginContext(client, event, contextData);
34+
({
35+
contextData,
36+
context,
37+
client,
38+
event} = createFixture());
5039
});
5140

5241
function processError(error) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { ContextData } from '../ContextData';
2+
import { EventPluginContext } from '../EventPluginContext';
3+
import { IEvent } from '../../models/IEvent';
4+
import { IError } from '../../models/IError';
5+
import { IErrorParser } from '../../services/IErrorParser';
6+
import { IStackFrame } from '../../models/IStackFrame';
7+
8+
export function createFixture() {
9+
let contextData: ContextData;
10+
let context: EventPluginContext;
11+
let errorParser: IErrorParser;
12+
let client: any;
13+
let event: IEvent;
14+
15+
errorParser = {
16+
parse: (c: EventPluginContext, exception: Error) => ({
17+
type: exception.name,
18+
message: exception.message,
19+
stack_trace: (<any>exception).testStack || null
20+
})
21+
};
22+
client = {
23+
config: {
24+
errorParser,
25+
log: {
26+
info: () => { }
27+
}
28+
}
29+
};
30+
event = {
31+
data: {}
32+
};
33+
contextData = new ContextData();
34+
context = new EventPluginContext(client, event, contextData);
35+
36+
return {
37+
contextData,
38+
context,
39+
client,
40+
event
41+
}
42+
}

src/plugins/default/ModuleInfoPlugin.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { EventPluginContext } from '../EventPluginContext';
33
import { IModule } from '../../models/IModule';
44

55
export class ModuleInfoPlugin implements IEventPlugin {
6-
public priority:number = 40;
6+
public priority:number = 50;
77
public name:string = 'ModuleInfoPlugin';
88

99
public run(context:EventPluginContext, next?:() => void): void {

src/plugins/default/RequestInfoPlugin.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { EventPluginContext } from '../EventPluginContext';
33
import { IRequestInfo } from '../../models/IRequestInfo';
44

55
export class RequestInfoPlugin implements IEventPlugin {
6-
public priority:number = 60;
6+
public priority:number = 70;
77
public name:string = 'RequestInfoPlugin';
88

99
public run(context:EventPluginContext, next?:() => void): void {

0 commit comments

Comments
 (0)