Skip to content

Commit

Permalink
Merge pull request #483 from airbrake/feauture/inc-request
Browse files Browse the repository at this point in the history
Add IncRequest API
  • Loading branch information
vmihailenco committed Oct 12, 2018
2 parents 205f4e7 + e2c96b8 commit c4197ec
Show file tree
Hide file tree
Showing 11 changed files with 366 additions and 275 deletions.
85 changes: 45 additions & 40 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ import angularMessageFilter from './filter/angular_message';
import windowFilter from './filter/window';
import nodeFilter from './filter/node';

import {Reporter, defaultReporter} from './reporter/reporter';
import fetchReporter from './reporter/fetch';
import requestReporter from './reporter/request';
import {Requester, makeRequester} from './http_req';

import {Historian, HistorianOptions} from './historian';
import Options from './options';
import {Historian} from './historian';
import {Routes} from './routes';


declare const VERSION: string;
Expand All @@ -31,30 +31,20 @@ interface Todo {
reject: (Error) => void;
}

interface Options {
projectId: number;
projectKey: string;
environment?: string;
host?: string;
timeout?: number;
keysBlacklist?: any[];
ignoreWindowError?: boolean;
processor?: Processor;
reporter?: Reporter;
instrumentation?: HistorianOptions;
}

class Client {
private opts: any;
private opts: Options;
private url: string;
private historian: Historian;

private processor: Processor;
private reporter: Reporter;
private requester: Requester;
private filters: Filter[] = [];

private offline = false;
private todo: Todo[] = [];

private routes: Routes;

private onClose: (() => void)[] = [];

constructor(opts: Options = {} as Options) {
Expand All @@ -69,21 +59,24 @@ class Client {
/password/,
/secret/,
];
this.url = `${this.opts.host}/api/v3/projects/${this.opts.projectId}/notices?key=${this.opts.projectKey}`;

this.processor = opts.processor || stacktracejsProcessor;
this.setReporter(opts.reporter || defaultReporter(opts));
this.processor = this.opts.processor || stacktracejsProcessor;
this.requester = makeRequester(this.opts);

this.addFilter(ignoreFilter);
this.addFilter(makeDebounceFilter());
this.addFilter(uncaughtMessageFilter);
this.addFilter(angularMessageFilter);

if (!opts.environment && process && process.env.NODE_ENV) {
opts.environment = process.env.NODE_ENV;
if (!this.opts.environment &&
typeof process !== 'undefined' &&
process.env.NODE_ENV) {
this.opts.environment = process.env.NODE_ENV;
}
if (opts.environment) {
if (this.opts.environment) {
this.addFilter((notice: Notice): Notice | null => {
notice.context.environment = opts.environment;
notice.context.environment = this.opts.environment;
return notice;
});
}
Expand Down Expand Up @@ -128,19 +121,6 @@ class Client {
this.historian.unregisterNotifier(this);
}

private setReporter(name: string|Reporter): void {
switch (name) {
case 'fetch':
this.reporter = fetchReporter;
break;
case 'request':
this.reporter = requestReporter;
break;
default:
this.reporter = name as Reporter;
}
}

addFilter(filter: Filter): void {
this.filters.push(filter);
}
Expand Down Expand Up @@ -216,9 +196,27 @@ class Client {
version: VERSION,
url: 'https://github.com/airbrake/airbrake-js'
};
return this.sendNotice(notice);
}

let payload = jsonifyNotice(notice, {keysBlacklist: this.opts.keysBlacklist});
return this.reporter(notice, payload, this.opts);
private sendNotice(notice: Notice): Promise<Notice> {
let body = jsonifyNotice(notice, {keysBlacklist: this.opts.keysBlacklist});
if (this.opts.reporter) {
return this.opts.reporter(notice);
}

let req = {
method: 'POST',
url: this.url,
body: body,
};
return this.requester(req).then((resp) => {
notice.id = resp.json.id;
return notice;
}).catch((err) => {
notice.error = err;
return notice;
});
}

// TODO: fix wrapping for multiple clients
Expand Down Expand Up @@ -276,6 +274,13 @@ class Client {
this.historian.onerror.apply(this.historian, arguments);
}

incRequest(method: string, route: string, statusCode: number, time: Date, ms: number): void {
if (!this.routes) {
this.routes = new Routes(this.opts);
}
this.routes.incRequest(method, route, statusCode, time, ms);
}

private onOnline(): void {
this.offline = false;

Expand Down
2 changes: 1 addition & 1 deletion src/historian.ts
Original file line number Diff line number Diff line change
Expand Up @@ -413,5 +413,5 @@ export class Historian {
}

function enabled(v: undefined|boolean): boolean {
return v === undefined || v === true
return v === undefined || v === true;
}
60 changes: 60 additions & 0 deletions src/http_req/fetch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
require('isomorphic-fetch');

import {HttpRequest, HttpResponse, errors} from './index';


let rateLimitReset = 0;


export function request(req: HttpRequest): Promise<HttpResponse> {
let utime = Date.now() / 1000;
if (utime < rateLimitReset) {
return Promise.reject(errors.ipRateLimited);
}

let opt = {
method: req.method,
body: req.body,
};
return fetch(req.url, opt).then((resp: Response) => {
if (resp.status === 401) {
throw errors.unauthorized;
}

if (resp.status === 429) {
let s = resp.headers.get('X-RateLimit-Delay');
if (!s) {
throw errors.ipRateLimited;
}

let n = parseInt(s, 10);
if (n > 0) {
rateLimitReset = Date.now() / 1000 + n;
}

throw errors.ipRateLimited;
}

if (resp.status === 204) {
return {json: null};
}
if (resp.status >= 200 && resp.status < 300) {
return resp.json().then((json) => {
return {json: json};
});
}

if (resp.status >= 400 && resp.status < 500) {
return resp.json().then((json) => {
let err = new Error(json.message);
throw err;
});
}

return resp.text().then((body) => {
let err = new Error(
`airbrake: fetch: unexpected response: code=${resp.status} body='${body}'`);
throw err;
});
});
}
28 changes: 28 additions & 0 deletions src/http_req/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import Options from '../options';
import {request as fetchRequest} from './fetch';
import {makeRequester as makeNodeRequester} from './node';

export interface HttpRequest {
method: string;
url: string;
body: string;
timeout?: number;
}

export interface HttpResponse {
json: any;
}

export type Requester = (req: HttpRequest) => Promise<HttpResponse>;

export function makeRequester(opts: Options): Requester {
if (opts.request) {
return makeNodeRequester(opts.request);
}
return fetchRequest;
}

export let errors = {
unauthorized: new Error('airbrake: unauthorized: project id or key are wrong'),
ipRateLimited: new Error('airbrake: IP is rate limited'),
};
111 changes: 111 additions & 0 deletions src/http_req/node.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import * as request_lib from 'request';

import {Requester, HttpRequest, HttpResponse, errors} from './index';


type requestAPI = request_lib.RequestAPI<request_lib.Request, request_lib.CoreOptions, request_lib.RequiredUriUrl>;

export function makeRequester(api: requestAPI): Requester {
return (req: HttpRequest): Promise<HttpResponse> => {
return request(req, api);
};
}


let rateLimitReset = 0;

function request(req: HttpRequest, api: requestAPI): Promise<HttpResponse> {
let utime = Date.now() / 1000;
if (utime < rateLimitReset) {
return Promise.reject(errors.ipRateLimited);
}

return new Promise((resolve, reject) => {
api({
url: req.url,
method: req.method,
body: req.body,
headers: {
'content-type': 'application/json'
},
timeout: req.timeout
}, function (error: any, resp: request_lib.RequestResponse, body: any): void {
if (error) {
reject(error);
return;
}

if (!resp.statusCode) {
let err = new Error(
`airbrake: request: response statusCode is ${resp.statusCode}`);
reject(err);
return;
}

if (resp.statusCode === 401) {
reject(errors.unauthorized);
return;
}

if (resp.statusCode === 429) {
reject(errors.ipRateLimited);

let h = resp.headers['x-ratelimit-delay'];
if (!h) {
return;
}

let s: string;
if (typeof h === 'string') {
s = h;
} else if (h instanceof Array) {
s = h[0];
} else {
return;
}

let n = parseInt(s, 10);
if (n > 0) {
rateLimitReset = Date.now() / 1000 + n;
}

return;
}

if (resp.statusCode === 204) {
resolve({json: null});
return;
}

if (resp.statusCode >= 200 && resp.statusCode < 300) {
let json;
try {
json = JSON.parse(body);
} catch (err) {
reject(err);
return;
}
resolve(json);
return;
}

if (resp.statusCode >= 400 && resp.statusCode < 500) {
let json;
try {
json = JSON.parse(body);
} catch (err) {
reject(err);
return;
}
let err = new Error(json.message);
reject(err);
return;
}

body = body.trim();
let err = new Error(
`airbrake: node: unexpected response: code=${resp.statusCode} body='${body}'`);
reject(err);
});
});
}
22 changes: 22 additions & 0 deletions src/options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import * as request from 'request';

import Notice from './notice';
import {HistorianOptions} from './historian';
import Processor from './processor/processor';

type Reporter = (notice: Notice) => Promise<Notice>;

export default interface Options {
projectId: number;
projectKey: string;
environment?: string;
host?: string;
timeout?: number;
keysBlacklist?: any[];
ignoreWindowError?: boolean;
processor?: Processor;
reporter?: Reporter;
instrumentation?: HistorianOptions;

request?: request.RequestAPI<request.Request, request.CoreOptions, request.RequiredUriUrl>;
}

0 comments on commit c4197ec

Please sign in to comment.