From 936374c2edc0fab65ea852a0772c1d9b31b03656 Mon Sep 17 00:00:00 2001 From: Pangratios Cosma Date: Fri, 17 Nov 2023 20:47:07 +0200 Subject: [PATCH] feat(js): read config from file (#1250) --- packages/js/src/server.ts | 16 ++- packages/js/src/server/util.ts | 12 ++- packages/js/test/unit/server.test.ts | 143 +++++++++++++++++++++++++++ 3 files changed, 163 insertions(+), 8 deletions(-) diff --git a/packages/js/src/server.ts b/packages/js/src/server.ts index 45f74173..9be441cd 100644 --- a/packages/js/src/server.ts +++ b/packages/js/src/server.ts @@ -12,6 +12,10 @@ import { CheckinsConfig } from './server/checkins-manager/types' import { CheckinsClient } from './server/checkins-manager/client'; const { endpoint } = Util +const DEFAULT_PLUGINS = [ + uncaughtException(), + unhandledRejection() +] type HoneybadgerServerConfig = (Types.Config | Types.ServerlessConfig) & CheckinsConfig @@ -61,8 +65,13 @@ class Honeybadger extends Client { } factory(opts?: Partial): this { + const clone = new Honeybadger({ + // fixme: this can create unwanted side-effects, needs to be tested thoroughly before enabling + // __plugins: DEFAULT_PLUGINS, + ...(readConfigFromFileSystem() ?? {}), + ...opts, // eslint-disable-next-line @typescript-eslint/no-explicit-any - const clone = new Honeybadger(opts) as any + }) as any clone.setNotifier(this.getNotifier()) return clone @@ -160,10 +169,7 @@ class Honeybadger extends Client { } const singleton = new Honeybadger({ - __plugins: [ - uncaughtException(), - unhandledRejection() - ], + __plugins: DEFAULT_PLUGINS, ...(readConfigFromFileSystem() ?? {}) }) diff --git a/packages/js/src/server/util.ts b/packages/js/src/server/util.ts index 2e988f11..3b584701 100644 --- a/packages/js/src/server/util.ts +++ b/packages/js/src/server/util.ts @@ -79,8 +79,14 @@ export function readConfigFromFileSystem(): Record { } function readConfigForModule(moduleName: string): Record { - const fileExplorer = cosmiconfigSync(moduleName) - const result = fileExplorer.search() + try { + const fileExplorer = cosmiconfigSync(moduleName) + const result = fileExplorer.search() - return result?.config ?? null + return result?.config ?? null + } + catch (e) { + console.debug(`[Honeybadger] Could not read config for module ${moduleName}: ${e.message}`) + return null + } } diff --git a/packages/js/test/unit/server.test.ts b/packages/js/test/unit/server.test.ts index 28b2d16f..ae74119b 100644 --- a/packages/js/test/unit/server.test.ts +++ b/packages/js/test/unit/server.test.ts @@ -1,9 +1,32 @@ import { Client as BaseClient } from '@honeybadger-io/core' +import os from 'os'; +import path from 'path'; import nock from 'nock' import Singleton from '../../src/server' import { nullLogger } from './helpers' import { CheckinDto, CheckinResponsePayload } from '../../src/server/checkins-manager/types'; import { Checkin } from '../../src/server/checkins-manager/checkin'; +import { cosmiconfigSync } from 'cosmiconfig' + +const configPath = path.resolve(__dirname, '../../', 'honeybadger.config.js') +jest.mock('cosmiconfig', () => ({ + cosmiconfigSync: jest.fn().mockImplementation((_moduleName, _options) => { + return { + load: jest.fn(), + clearCaches: jest.fn(), + clearLoadCache: jest.fn(), + clearSearchCache: jest.fn(), + search: () => { + return { + isEmpty: true, + config: {}, + filepath: null + } + } + } + }) +})) +const mockCosmiconfig = cosmiconfigSync as jest.MockedFunction describe('server client', function () { let client: typeof Singleton @@ -15,6 +38,126 @@ describe('server client', function () { }) }) + afterEach(() => { + jest.resetAllMocks() + }) + + describe('configuration', function () { + it('creates a client with default configuration', function () { + const client = Singleton.factory() + expect(client.config).toMatchObject({ + apiKey: null, + endpoint: 'https://api.honeybadger.io', + environment: null, + projectRoot: process.cwd(), + hostname: os.hostname(), + component: null, + action: null, + revision: null, + reportData: null, + breadcrumbsEnabled: true, + maxBreadcrumbs: 40, + maxObjectDepth: 8, + logger: console, + developmentEnvironments: ['dev', 'development', 'test'], + debug: false, + tags: null, + enableUncaught: true, + enableUnhandledRejection: true, + reportTimeoutWarning: true, + timeoutWarningThresholdMs: 50, + filters: ['creditcard', 'password'], + __plugins: [], + }) + }) + + it('creates a client with constructor arguments', function () { + const opts = { + apiKey: 'testing', + environment: 'staging', + developmentEnvironments: ['staging', 'dev', 'local'], + tags: ['tag-1', 'tag-2'] + } + const client = Singleton.factory(opts) + expect(client.config).toMatchObject(opts) + }) + + it('creates a client from a configuration file', function () { + const configFromFile = { + apiKey: 'testing', + personalAuthToken: 'p123', + environment: 'staging', + developmentEnvironments: ['staging', 'dev', 'local'], + tags: ['tag-1', 'tag-2'], + checkins: [ + { + projectId: '11111', + name: 'a check-in', + scheduleType: 'simple', + reportPeriod: '1 week', + gracePeriod: '5 minutes' + } + ] + } + mockCosmiconfig.mockImplementation((_moduleName, _options) => { + return { + load: jest.fn(), + clearCaches: jest.fn(), + clearLoadCache: jest.fn(), + clearSearchCache: jest.fn(), + search: () => { + return { + config: configFromFile, + filepath: configPath + } + } + } + }) + const client = Singleton.factory() + expect(client.config).toMatchObject(configFromFile) + }) + + it('creates a client from both a configuration file and constructor arguments', function () { + const configFromFile = { + apiKey: 'testing', + personalAuthToken: 'p123', + environment: 'staging', + developmentEnvironments: ['staging', 'dev', 'local'], + tags: ['tag-1', 'tag-2'], + checkins: [ + { + projectId: '11111', + name: 'a check-in', + scheduleType: 'simple', + reportPeriod: '1 week', + gracePeriod: '5 minutes' + } + ] + } + mockCosmiconfig.mockImplementation((_moduleName, _options) => { + return { + load: jest.fn(), + clearCaches: jest.fn(), + clearLoadCache: jest.fn(), + clearSearchCache: jest.fn(), + search: () => { + return { + config: configFromFile, + filepath: configPath + } + } + } + }) + const client = Singleton.factory({ + apiKey: 'not-testing' + }) + expect(client.config).toMatchObject({ + ...configFromFile, + apiKey: 'not-testing' + }) + }) + }) + it('inherits from base client', function () { expect(client).toEqual(expect.any(BaseClient)) })