Skip to content

Commit

Permalink
Add support for remote notifier config
Browse files Browse the repository at this point in the history
  • Loading branch information
kyrylo committed Feb 18, 2021
1 parent f40a436 commit 78387b6
Show file tree
Hide file tree
Showing 14 changed files with 515 additions and 8 deletions.
43 changes: 43 additions & 0 deletions packages/browser/src/base_notifier.ts
Expand Up @@ -22,6 +22,7 @@ import { QueueMetric, QueuesStats } from './queues';
import { RouteMetric, RoutesBreakdowns, RoutesStats } from './routes';
import { NOTIFIER_NAME, NOTIFIER_VERSION, NOTIFIER_URL } from './version';
import { PerformanceFilter } from './filter/performance_filter';
import { RemoteSettings } from './remote_settings';

export class BaseNotifier {
routes: Routes;
Expand All @@ -46,10 +47,17 @@ export class BaseNotifier {

this._opt = opt;
this._opt.host = this._opt.host || 'https://api.airbrake.io';
this._opt.remoteConfigHost =
this._opt.remoteConfigHost || 'https://notifier-configs.airbrake.io';
this._opt.apmHost = this._opt.apmHost || 'https://api.airbrake.io';
this._opt.timeout = this._opt.timeout || 10000;
this._opt.keysBlocklist = this._opt.keysBlocklist || [/password/, /secret/];
this._url = `${this._opt.host}/api/v3/projects/${this._opt.projectId}/notices?key=${this._opt.projectKey}`;

this._opt.errorNotifications = this._opt.errorNotifications !== false;
this._opt.performanceStats = this._opt.performanceStats !== false;
this._opt.remoteConfig = this._opt.remoteConfig !== false;

this._processor = this._opt.processor || espProcessor;
this._requester = makeRequester(this._opt);

Expand All @@ -73,6 +81,11 @@ export class BaseNotifier {
this.routes = new Routes(this);
this.queues = new Queues(this);
this.queries = new QueriesStats(this._opt);

if (this._opt.remoteConfig) {
const pollerId = new RemoteSettings(this._opt).poll();
this._onClose.push(() => clearInterval(pollerId));
}
}

close(): void {
Expand Down Expand Up @@ -114,6 +127,15 @@ export class BaseNotifier {
err = { error: err };
}

if (!this._opt.errorNotifications) {
notice.error = new Error(
`airbrake: not sending this error, errorNotifications is disabled err=${JSON.stringify(
err.error
)}`
);
return Promise.resolve(notice);
}

if (!err.error) {
notice.error = new Error(
`airbrake: got err=${JSON.stringify(err.error)}, wanted an Error`
Expand Down Expand Up @@ -227,11 +249,13 @@ class Routes {
_notifier: BaseNotifier;
_routes: RoutesStats;
_breakdowns: RoutesBreakdowns;
_opt: IOptions;

constructor(notifier: BaseNotifier) {
this._notifier = notifier;
this._routes = new RoutesStats(notifier._opt);
this._breakdowns = new RoutesBreakdowns(notifier._opt);
this._opt = notifier._opt;
}

start(
Expand All @@ -242,6 +266,10 @@ class Routes {
): RouteMetric {
const metric = new RouteMetric(method, route, statusCode, contentType);

if (!this._opt.performanceStats) {
return metric;
}

const scope = this._notifier.scope().clone();
scope.setContext({ httpMethod: method, route });
scope.setRouteMetric(metric);
Expand All @@ -251,7 +279,12 @@ class Routes {
}

notify(req: RouteMetric): void {
if (!this._opt.performanceStats) {
return;
}

req.end();

for (const performanceFilter of this._notifier._performanceFilters) {
if (performanceFilter(req) === null) {
return;
Expand All @@ -265,15 +298,21 @@ class Routes {
class Queues {
_notifier: BaseNotifier;
_queues: QueuesStats;
_opt: IOptions;

constructor(notifier: BaseNotifier) {
this._notifier = notifier;
this._queues = new QueuesStats(notifier._opt);
this._opt = notifier._opt;
}

start(queue: string): QueueMetric {
const metric = new QueueMetric(queue);

if (!this._opt.performanceStats) {
return metric;
}

const scope = this._notifier.scope().clone();
scope.setContext({ queue });
scope.setQueueMetric(metric);
Expand All @@ -283,6 +322,10 @@ class Queues {
}

notify(q: QueueMetric): void {
if (!this._opt.performanceStats) {
return;
}

q.end();
this._queues.notify(q);
}
Expand Down
3 changes: 2 additions & 1 deletion packages/browser/src/http_req/api.ts
@@ -1,8 +1,9 @@
export interface IHttpRequest {
method: string;
url: string;
body: string;
body?: string;
timeout?: number;
headers?: any;
}

export interface IHttpResponse {
Expand Down
1 change: 1 addition & 0 deletions packages/browser/src/http_req/fetch.ts
Expand Up @@ -13,6 +13,7 @@ export function request(req: IHttpRequest): Promise<IHttpResponse> {
let opt = {
method: req.method,
body: req.body,
headers: req.headers,
};
return fetch(req.url, opt).then((resp: Response) => {
if (resp.status === 401) {
Expand Down
4 changes: 4 additions & 0 deletions packages/browser/src/options.ts
Expand Up @@ -18,11 +18,15 @@ export interface IOptions {
projectKey: string;
environment?: string;
host?: string;
apmHost?: string;
remoteConfigHost?: string;
remoteConfig?: boolean;
timeout?: number;
keysBlocklist?: any[];
processor?: Processor;
reporter?: Reporter;
instrumentation?: IInstrumentationOptions;
errorNotifications?: boolean;
performanceStats?: boolean;

request?: request.RequestAPI<
Expand Down
4 changes: 4 additions & 0 deletions packages/browser/src/queries.ts
Expand Up @@ -59,6 +59,10 @@ export class QueriesStats {
return;
}

if (!this._opt.performanceStats) {
return;
}

let ms = q._duration();

const minute = 60 * 1000;
Expand Down
4 changes: 4 additions & 0 deletions packages/browser/src/queues.ts
Expand Up @@ -39,6 +39,10 @@ export class QueuesStats {
return;
}

if (!this._opt.performanceStats) {
return;
}

let ms = q._duration();
if (ms === 0) {
ms = 0.00001;
Expand Down
204 changes: 204 additions & 0 deletions packages/browser/src/remote_settings.ts
@@ -0,0 +1,204 @@
import { makeRequester, Requester } from './http_req';
import { IOptions } from './options';
import { NOTIFIER_NAME, NOTIFIER_VERSION } from './version';

// API version to poll.
const API_VER = '2020-06-18';

// How frequently we should poll the config API.
const DEFAULT_INTERVAL = 600000; // 10 minutes

const NOTIFIER_INFO = {
notifier_name: NOTIFIER_NAME,
notifier_version: NOTIFIER_VERSION,
os:
typeof window !== 'undefined' &&
window.navigator &&
window.navigator.userAgent
? window.navigator.userAgent
: undefined,
language: 'JavaScript',
};

// Remote config settings.
const ERROR_SETTING = 'errors';
const APM_SETTING = 'apm';

interface IRemoteConfig {
project_id: number;
updated_at: number;
poll_sec: number;
config_route: string;
settings: IRemoteConfigSetting[];
}

interface IRemoteConfigSetting {
name: string;
enabled: boolean;
endpoint: string;
}

export class RemoteSettings {
_opt: IOptions;
_requester: Requester;
_data: SettingsData;
_origErrorNotifications: boolean;
_origPerformanceStats: boolean;

constructor(opt: IOptions) {
this._opt = opt;
this._requester = makeRequester(opt);

this._data = new SettingsData(opt.projectId, {
project_id: null,
poll_sec: 0,
updated_at: 0,
config_route: '',
settings: [],
});

this._origErrorNotifications = opt.errorNotifications;
this._origPerformanceStats = opt.performanceStats;
}

poll(): any {
// First request is immediate. When it's done, we cancel it since we want to
// change interval time to the default value.
const pollerId = setInterval(() => {
this._doRequest();
clearInterval(pollerId);
}, 0);

// Second fetch is what always runs in background.
return setInterval(this._doRequest.bind(this), DEFAULT_INTERVAL);
}

_doRequest(): void {
this._requester(this._requestParams(this._opt)).then((resp) => {
this._data.merge(resp.json);

this._opt.host = this._data.errorHost();
this._opt.apmHost = this._data.apmHost();

this._processErrorNotifications(this._data);
this._processPerformanceStats(this._data);
});
}

_requestParams(opt: IOptions): any {
return {
method: 'GET',
url: this._pollUrl(opt),
headers: {
Accept: 'application/json',
'Cache-Control': 'no-cache,no-store',
},
};
}

_pollUrl(opt: IOptions): string {
const url = new URL(this._data.configRoute(opt.remoteConfigHost));

for (const [key, value] of Object.entries(NOTIFIER_INFO)) {
url.searchParams.append(key, value);
}

return url.toString();
}

_processErrorNotifications(data: SettingsData): void {
if (!this._origErrorNotifications) {
return;
}
this._opt.errorNotifications = data.errorNotifications();
}

_processPerformanceStats(data: SettingsData): void {
if (!this._origPerformanceStats) {
return;
}
this._opt.performanceStats = data.performanceStats();
}
}

export class SettingsData {
_projectId: number;
_data: IRemoteConfig;

constructor(projectId: number, data: IRemoteConfig) {
this._projectId = projectId;
this._data = data;
}

merge(other: IRemoteConfig) {
this._data = { ...this._data, ...other };
}

configRoute(remoteConfigHost: string): string {
const host = remoteConfigHost.replace(/\/$/, '');
const configRoute = this._data.config_route;

if (
configRoute === null ||
configRoute === undefined ||
configRoute === ''
) {
return `${host}/${API_VER}/config/${this._projectId}/config.json`;
} else {
return `${host}/${configRoute}`;
}
}

errorNotifications(): boolean {
const s = this._findSetting(ERROR_SETTING);
if (s === null) {
return true;
}

return s.enabled;
}

performanceStats(): boolean {
const s = this._findSetting(APM_SETTING);
if (s === null) {
return true;
}

return s.enabled;
}

errorHost(): string {
const s = this._findSetting(ERROR_SETTING);
if (s === null) {
return null;
}

return s.endpoint;
}

apmHost(): string {
const s = this._findSetting(APM_SETTING);
if (s === null) {
return null;
}

return s.endpoint;
}

_findSetting(name: string): IRemoteConfigSetting {
const settings = this._data.settings;
if (settings === null || settings === undefined) {
return null;
}

const setting = settings.find((s) => {
return s.name === name;
});

if (setting === undefined) {
return null;
}

return setting;
}
}

0 comments on commit 78387b6

Please sign in to comment.