diff --git a/lib/event_processor/batch_event_processor.ts b/lib/event_processor/batch_event_processor.ts index 40282a8f3..baf7a2d86 100644 --- a/lib/event_processor/batch_event_processor.ts +++ b/lib/event_processor/batch_event_processor.ts @@ -27,8 +27,10 @@ import { isSuccessStatusCode } from "../utils/http_request_handler/http_util"; import { EventEmitter } from "../utils/event_emitter/event_emitter"; import { IdGenerator } from "../utils/id_generator"; import { areEventContextsEqual } from "./event_builder/user_event"; -import { EVENT_PROCESSOR_STOPPED, FAILED_TO_DISPATCH_EVENTS, FAILED_TO_DISPATCH_EVENTS_WITH_ARG } from "error_message"; +import { FAILED_TO_DISPATCH_EVENTS, SERVICE_NOT_RUNNING } from "error_message"; import { OptimizelyError } from "../error/optimizly_error"; +import { sprintf } from "../utils/fns"; +import { SERVICE_STOPPED_BEFORE_RUNNING } from "../service"; export const DEFAULT_MIN_BACKOFF = 1000; export const DEFAULT_MAX_BACKOFF = 32000; @@ -174,7 +176,7 @@ export class BatchEventProcessor extends BaseService implements EventProcessor { const dispatcher = closing && this.closingEventDispatcher ? this.closingEventDispatcher : this.eventDispatcher; return dispatcher.dispatchEvent(request).then((res) => { if (res.statusCode && !isSuccessStatusCode(res.statusCode)) { - return Promise.reject(new OptimizelyError(FAILED_TO_DISPATCH_EVENTS_WITH_ARG, res.statusCode)); + return Promise.reject(new OptimizelyError(FAILED_TO_DISPATCH_EVENTS, res.statusCode)); } return Promise.resolve(res); }); @@ -209,7 +211,7 @@ export class BatchEventProcessor extends BaseService implements EventProcessor { }).catch((err) => { // if the dispatch fails, the events will still be // in the store for future processing - this.logger?.error(FAILED_TO_DISPATCH_EVENTS, err); + this.logger?.error(err); }).finally(() => { this.runningTask.delete(taskId); ids.forEach((id) => this.dispatchingEventIds.delete(id)); @@ -228,7 +230,7 @@ export class BatchEventProcessor extends BaseService implements EventProcessor { async process(event: ProcessableEvent): Promise { if (!this.isRunning()) { - return Promise.reject('Event processor is not running'); + return Promise.reject(new OptimizelyError(SERVICE_NOT_RUNNING, 'BatchEventProcessor')); } const eventWithId = { @@ -285,7 +287,9 @@ export class BatchEventProcessor extends BaseService implements EventProcessor { } if (this.isNew()) { - this.startPromise.reject(new OptimizelyError(EVENT_PROCESSOR_STOPPED)); + this.startPromise.reject(new Error( + sprintf(SERVICE_STOPPED_BEFORE_RUNNING, 'BatchEventProcessor') + )); } this.state = ServiceState.Stopping; diff --git a/lib/event_processor/forwarding_event_processor.ts b/lib/event_processor/forwarding_event_processor.ts index 744ac5975..a0587ab6a 100644 --- a/lib/event_processor/forwarding_event_processor.ts +++ b/lib/event_processor/forwarding_event_processor.ts @@ -23,8 +23,8 @@ import { buildLogEvent } from './event_builder/log_event'; import { BaseService, ServiceState } from '../service'; import { EventEmitter } from '../utils/event_emitter/event_emitter'; import { Consumer, Fn } from '../utils/type'; -import { SERVICE_STOPPED_BEFORE_RUNNING } from 'error_message'; -import { OptimizelyError } from '../error/optimizly_error'; +import { SERVICE_STOPPED_BEFORE_RUNNING } from '../service'; +import { sprintf } from '../utils/fns'; class ForwardingEventProcessor extends BaseService implements EventProcessor { private dispatcher: EventDispatcher; @@ -57,7 +57,9 @@ class ForwardingEventProcessor extends BaseService implements EventProcessor { } if (this.isNew()) { - this.startPromise.reject(new OptimizelyError(SERVICE_STOPPED_BEFORE_RUNNING)); + this.startPromise.reject(new Error( + sprintf(SERVICE_STOPPED_BEFORE_RUNNING, 'ForwardingEventProcessor')) + ); } this.state = ServiceState.Terminated; diff --git a/lib/message/error_message.ts b/lib/message/error_message.ts index d820f59ee..b47e718bf 100644 --- a/lib/message/error_message.ts +++ b/lib/message/error_message.ts @@ -41,7 +41,6 @@ export const NO_EVENT_PROCESSOR = 'No event processor is provided'; export const NO_VARIATION_FOR_EXPERIMENT_KEY = 'No variation key %s defined in datafile for experiment %s.'; export const ODP_CONFIG_NOT_AVAILABLE = 'ODP config is not available.'; export const ODP_EVENT_FAILED = 'ODP event send failed.'; -export const ODP_EVENT_MANAGER_IS_NOT_RUNNING = 'ODP event manager is not running.'; export const ODP_EVENTS_SHOULD_HAVE_ATLEAST_ONE_KEY_VALUE = 'ODP events should have at least one key-value pair in identifiers.'; export const ODP_EVENT_FAILED_ODP_MANAGER_MISSING = 'ODP Event failed to send. (ODP Manager not available).'; export const ODP_NOT_INTEGRATED = 'ODP is not integrated'; @@ -89,25 +88,16 @@ export const REQUEST_TIMEOUT = 'Request timeout'; export const REQUEST_ERROR = 'Request error'; export const NO_STATUS_CODE_IN_RESPONSE = 'No status code in response'; export const UNSUPPORTED_PROTOCOL = 'Unsupported protocol: %s'; -export const ONREADY_TIMEOUT = 'onReady timeout expired after %s ms'; -export const INSTANCE_CLOSED = 'Instance closed'; -export const DATAFILE_MANAGER_STOPPED = 'Datafile manager stopped before it could be started'; -export const FAILED_TO_FETCH_DATAFILE = 'Failed to fetch datafile'; -export const NO_SDKKEY_OR_DATAFILE = 'At least one of sdkKey or datafile must be provided'; export const RETRY_CANCELLED = 'Retry cancelled'; -export const SERVICE_STOPPED_BEFORE_RUNNING = 'Service stopped before running'; export const ONLY_POST_REQUESTS_ARE_SUPPORTED = 'Only POST requests are supported'; export const SEND_BEACON_FAILED = 'sendBeacon failed'; -export const FAILED_TO_DISPATCH_EVENTS = 'Failed to dispatch events' -export const FAILED_TO_DISPATCH_EVENTS_WITH_ARG = 'Failed to dispatch events: %s'; -export const EVENT_PROCESSOR_STOPPED = 'Event processor stopped before it could be started'; -export const ODP_MANAGER_STOPPED_BEFORE_RUNNING = 'odp manager stopped before running'; +export const FAILED_TO_DISPATCH_EVENTS = 'Failed to dispatch events, status: %s'; export const ODP_EVENT_MANAGER_STOPPED = "ODP event manager stopped before it could start"; -export const DATAFILE_MANAGER_FAILED_TO_START = 'Datafile manager failed to start'; export const UNABLE_TO_ATTACH_UNLOAD = 'unable to bind optimizely.close() to page unload event: "%s"'; export const UNABLE_TO_PARSE_AND_SKIPPED_HEADER = 'Unable to parse & skipped header item'; export const CMAB_FETCH_FAILED = 'CMAB decision fetch failed with status: %s'; export const INVALID_CMAB_FETCH_RESPONSE = 'Invalid CMAB fetch response'; export const PROMISE_NOT_ALLOWED = "Promise value is not allowed in sync operation"; +export const SERVICE_NOT_RUNNING = "%s not running"; export const messages: string[] = []; diff --git a/lib/odp/event_manager/odp_event_manager.ts b/lib/odp/event_manager/odp_event_manager.ts index a076655b5..3a9c591cc 100644 --- a/lib/odp/event_manager/odp_event_manager.ts +++ b/lib/odp/event_manager/odp_event_manager.ts @@ -27,14 +27,16 @@ import { EVENT_ACTION_INVALID, EVENT_DATA_INVALID, FAILED_TO_SEND_ODP_EVENTS, - ODP_EVENT_MANAGER_IS_NOT_RUNNING, ODP_EVENTS_SHOULD_HAVE_ATLEAST_ONE_KEY_VALUE, ODP_NOT_INTEGRATED, - FAILED_TO_DISPATCH_EVENTS_WITH_ARG, - ODP_EVENT_MANAGER_STOPPED + FAILED_TO_DISPATCH_EVENTS, + ODP_EVENT_MANAGER_STOPPED, + SERVICE_NOT_RUNNING } from 'error_message'; import { OptimizelyError } from '../../error/optimizly_error'; import { LoggerFacade } from '../../logging/logger'; +import { SERVICE_STOPPED_BEFORE_RUNNING } from '../../service'; +import { sprintf } from '../../utils/fns'; export interface OdpEventManager extends Service { updateConfig(odpIntegrationConfig: OdpIntegrationConfig): void; @@ -86,7 +88,7 @@ export class DefaultOdpEventManager extends BaseService implements OdpEventManag private async executeDispatch(odpConfig: OdpConfig, batch: OdpEvent[]): Promise { const res = await this.apiManager.sendEvents(odpConfig, batch); if (res.statusCode && !isSuccessStatusCode(res.statusCode)) { - return Promise.reject(new OptimizelyError(FAILED_TO_DISPATCH_EVENTS_WITH_ARG, res.statusCode)); + return Promise.reject(new OptimizelyError(FAILED_TO_DISPATCH_EVENTS, res.statusCode)); } return await Promise.resolve(res); } @@ -113,7 +115,7 @@ export class DefaultOdpEventManager extends BaseService implements OdpEventManag } start(): void { - if (!this.isNew) { + if (!this.isNew()) { return; } @@ -164,7 +166,9 @@ export class DefaultOdpEventManager extends BaseService implements OdpEventManag } if (this.isNew()) { - this.startPromise.reject(new OptimizelyError(ODP_EVENT_MANAGER_STOPPED)); + this.startPromise.reject(new Error( + sprintf(SERVICE_STOPPED_BEFORE_RUNNING, 'OdpEventManager') + )); } this.flush(); @@ -174,7 +178,7 @@ export class DefaultOdpEventManager extends BaseService implements OdpEventManag sendEvent(event: OdpEvent): void { if (!this.isRunning()) { - this.logger?.error(ODP_EVENT_MANAGER_IS_NOT_RUNNING); + this.logger?.error(SERVICE_NOT_RUNNING, 'OdpEventManager'); return; } diff --git a/lib/odp/odp_manager.ts b/lib/odp/odp_manager.ts index 68a2b2c79..8baaed658 100644 --- a/lib/odp/odp_manager.ts +++ b/lib/odp/odp_manager.ts @@ -29,8 +29,8 @@ import { CLIENT_VERSION, JAVASCRIPT_CLIENT_ENGINE } from '../utils/enums'; import { ODP_DEFAULT_EVENT_TYPE, ODP_EVENT_ACTION, ODP_USER_KEY } from './constant'; import { isVuid } from '../vuid/vuid'; import { Maybe } from '../utils/type'; -import { ODP_MANAGER_STOPPED_BEFORE_RUNNING } from 'error_message'; -import { OptimizelyError } from '../error/optimizly_error'; +import { sprintf } from '../utils/fns'; +import { SERVICE_STOPPED_BEFORE_RUNNING } from '../service'; export interface OdpManager extends Service { updateConfig(odpIntegrationConfig: OdpIntegrationConfig): boolean; @@ -151,7 +151,9 @@ export class DefaultOdpManager extends BaseService implements OdpManager { } if (!this.isRunning()) { - this.startPromise.reject(new OptimizelyError(ODP_MANAGER_STOPPED_BEFORE_RUNNING)); + this.startPromise.reject(new Error( + sprintf(SERVICE_STOPPED_BEFORE_RUNNING, 'OdpManager') + )); } this.state = ServiceState.Stopping; diff --git a/lib/optimizely/index.tests.js b/lib/optimizely/index.tests.js index 5ec683a92..77ce8e0f1 100644 --- a/lib/optimizely/index.tests.js +++ b/lib/optimizely/index.tests.js @@ -44,10 +44,10 @@ import { NOT_TRACKING_USER, EVENT_KEY_NOT_FOUND, INVALID_EXPERIMENT_KEY, - ONREADY_TIMEOUT, SERVICE_STOPPED_BEFORE_RUNNING } from 'error_message'; +import { ONREADY_TIMEOUT, INSTANCE_CLOSED } from './'; import { AUDIENCE_EVALUATION_RESULT_COMBINED, USER_NOT_IN_EXPERIMENT, @@ -9455,8 +9455,7 @@ describe('lib/optimizely', function() { return readyPromise.then(() => { return Promise.reject(new Error('PROMISE_SHOULD_NOT_HAVE_RESOLVED')); }, (err) => { - assert.equal(err.baseMessage, ONREADY_TIMEOUT); - assert.deepEqual(err.params, [ 500 ]); + assert.equal(err.message, sprintf(ONREADY_TIMEOUT, 500)); }); }); @@ -9479,8 +9478,7 @@ describe('lib/optimizely', function() { return readyPromise.then(() => { return Promise.reject(new Error('PROMISE_SHOULD_NOT_HAVE_RESOLVED')); }, (err) => { - assert.equal(err.baseMessage, ONREADY_TIMEOUT); - assert.deepEqual(err.params, [ 30000 ]); + assert.equal(err.message, sprintf(ONREADY_TIMEOUT, 30000)); }); }); diff --git a/lib/optimizely/index.ts b/lib/optimizely/index.ts index 6895fcea7..09b7d47d9 100644 --- a/lib/optimizely/index.ts +++ b/lib/optimizely/index.ts @@ -59,7 +59,7 @@ import { NODE_CLIENT_ENGINE, CLIENT_VERSION, } from '../utils/enums'; -import { Fn, Maybe, OpType, OpValue } from '../utils/type'; +import { Fn, Maybe, OpType } from '../utils/type'; import { resolvablePromise } from '../utils/promise/resolvablePromise'; import { NOTIFICATION_TYPES, DecisionNotificationType, DECISION_NOTIFICATION_TYPES } from '../notification_center/type'; @@ -75,9 +75,6 @@ import { EVENT_KEY_NOT_FOUND, NOT_TRACKING_USER, VARIABLE_REQUESTED_WITH_WRONG_TYPE, - ONREADY_TIMEOUT, - INSTANCE_CLOSED, - SERVICE_STOPPED_BEFORE_RUNNING } from 'error_message'; import { @@ -98,6 +95,8 @@ import { VARIABLE_NOT_USED_RETURN_DEFAULT_VARIABLE_VALUE, } from 'log_message'; +import { SERVICE_STOPPED_BEFORE_RUNNING } from '../service'; + import { ErrorNotifier } from '../error/error_notifier'; import { ErrorReporter } from '../error/error_reporter'; import { OptimizelyError } from '../error/optimizly_error'; @@ -113,6 +112,9 @@ type StringInputs = Partial>; type DecisionReasons = (string | number)[]; +export const INSTANCE_CLOSED = 'Instance closed'; +export const ONREADY_TIMEOUT = 'onReady timeout expired after %s ms'; + /** * options required to create optimizely object */ @@ -1257,7 +1259,9 @@ export default class Optimizely extends BaseService implements Client { } if (!this.isRunning()) { - this.startPromise.reject(new OptimizelyError(SERVICE_STOPPED_BEFORE_RUNNING)); + this.startPromise.reject(new Error( + sprintf(SERVICE_STOPPED_BEFORE_RUNNING, 'Client') + )); } this.state = ServiceState.Stopping; @@ -1322,14 +1326,16 @@ export default class Optimizely extends BaseService implements Client { const onReadyTimeout = () => { this.cleanupTasks.delete(cleanupTaskId); - timeoutPromise.reject(new OptimizelyError(ONREADY_TIMEOUT, timeoutValue)); + timeoutPromise.reject(new Error( + sprintf(ONREADY_TIMEOUT, timeoutValue) + )); }; const readyTimeout = setTimeout(onReadyTimeout, timeoutValue); this.cleanupTasks.set(cleanupTaskId, () => { clearTimeout(readyTimeout); - timeoutPromise.reject(new OptimizelyError(INSTANCE_CLOSED)); + timeoutPromise.reject(new Error(INSTANCE_CLOSED)); }); return Promise.race([this.onRunning().then(() => { diff --git a/lib/project_config/polling_datafile_manager.ts b/lib/project_config/polling_datafile_manager.ts index bac6adc1e..fbbbeb0e0 100644 --- a/lib/project_config/polling_datafile_manager.ts +++ b/lib/project_config/polling_datafile_manager.ts @@ -24,10 +24,8 @@ import { Repeater } from '../utils/repeater/repeater'; import { Consumer, Fn } from '../utils/type'; import { isSuccessStatusCode } from '../utils/http_request_handler/http_util'; import { - DATAFILE_MANAGER_STOPPED, DATAFILE_FETCH_REQUEST_FAILED, ERROR_FETCHING_DATAFILE, - FAILED_TO_FETCH_DATAFILE, } from 'error_message'; import { ADDING_AUTHORIZATION_HEADER_WITH_BEARER_TOKEN, @@ -40,6 +38,10 @@ import { LoggerFacade } from '../logging/logger'; export const LOGGER_NAME = 'PollingDatafileManager'; +import { SERVICE_STOPPED_BEFORE_RUNNING } from '../service'; + +export const FAILED_TO_FETCH_DATAFILE = 'Failed to fetch datafile'; + export class PollingDatafileManager extends BaseService implements DatafileManager { private requestHandler: RequestHandler; private currentDatafile?: string; @@ -123,7 +125,9 @@ export class PollingDatafileManager extends BaseService implements DatafileManag } if (this.isNew() || this.isStarting()) { - this.startPromise.reject(new OptimizelyError(DATAFILE_MANAGER_STOPPED)); + this.startPromise.reject(new Error( + sprintf(SERVICE_STOPPED_BEFORE_RUNNING, 'PollingDatafileManager') + )); } this.state = ServiceState.Terminated; @@ -136,7 +140,7 @@ export class PollingDatafileManager extends BaseService implements DatafileManag private handleInitFailure(): void { this.state = ServiceState.Failed; this.repeater.stop(); - const error = new OptimizelyError(FAILED_TO_FETCH_DATAFILE); + const error = new Error(FAILED_TO_FETCH_DATAFILE); this.startPromise.reject(error); this.stopPromise.reject(error); } diff --git a/lib/project_config/project_config_manager.spec.ts b/lib/project_config/project_config_manager.spec.ts index 3e3236644..a8c98ece4 100644 --- a/lib/project_config/project_config_manager.spec.ts +++ b/lib/project_config/project_config_manager.spec.ts @@ -209,10 +209,10 @@ describe('ProjectConfigManagerImpl', () => { describe('when datafile is invalid', () => { it('should reject onRunning() with the same error if datafileManager.onRunning() rejects', async () => { - const datafileManager = getMockDatafileManager({ onRunning: Promise.reject('test error') }); + const datafileManager = getMockDatafileManager({ onRunning: Promise.reject(new Error('test error')) }); const manager = new ProjectConfigManagerImpl({ datafile: {}, datafileManager }); manager.start(); - await expect(manager.onRunning()).rejects.toBe('test error'); + await expect(manager.onRunning()).rejects.toThrow('DatafileManager failed to start, reason: test error'); }); it('should resolve onRunning() if datafileManager.onUpdate() is fired and should update config', async () => { @@ -258,10 +258,10 @@ describe('ProjectConfigManagerImpl', () => { describe('when datafile is not provided', () => { it('should reject onRunning() if datafileManager.onRunning() rejects', async () => { - const datafileManager = getMockDatafileManager({ onRunning: Promise.reject('test error') }); + const datafileManager = getMockDatafileManager({ onRunning: Promise.reject(new Error('test error')) }); const manager = new ProjectConfigManagerImpl({ datafileManager }); manager.start(); - await expect(manager.onRunning()).rejects.toBe('test error'); + await expect(manager.onRunning()).rejects.toThrow('DatafileManager failed to start, reason: test error'); }); it('should reject onRunning() and onTerminated if datafileManager emits an invalid datafile in the first onUpdate', async () => { diff --git a/lib/project_config/project_config_manager.ts b/lib/project_config/project_config_manager.ts index 95e3fa029..edf88a174 100644 --- a/lib/project_config/project_config_manager.ts +++ b/lib/project_config/project_config_manager.ts @@ -22,9 +22,16 @@ import { scheduleMicrotask } from '../utils/microtask'; import { Service, ServiceState, BaseService } from '../service'; import { Consumer, Fn, Transformer } from '../utils/type'; import { EventEmitter } from '../utils/event_emitter/event_emitter'; -import { DATAFILE_MANAGER_STOPPED, NO_SDKKEY_OR_DATAFILE, DATAFILE_MANAGER_FAILED_TO_START } from 'error_message'; -import { OptimizelyError } from '../error/optimizly_error'; +import { + SERVICE_FAILED_TO_START, + SERVICE_STOPPED_BEFORE_RUNNING, +} from '../service' + +export const NO_SDKKEY_OR_DATAFILE = 'sdkKey or datafile must be provided'; +export const GOT_INVALID_DATAFILE = 'got invalid datafile'; + +import { sprintf } from '../utils/fns'; interface ProjectConfigManagerConfig { datafile?: string | Record; jsonSchemaValidator?: Transformer, @@ -82,7 +89,7 @@ export class ProjectConfigManagerImpl extends BaseService implements ProjectConf this.state = ServiceState.Starting; if (!this.datafile && !this.datafileManager) { - this.handleInitError(new OptimizelyError(NO_SDKKEY_OR_DATAFILE)); + this.handleInitError(new Error(NO_SDKKEY_OR_DATAFILE)); return; } @@ -119,14 +126,16 @@ export class ProjectConfigManagerImpl extends BaseService implements ProjectConf } private handleDatafileManagerError(err: Error): void { - this.logger?.error(DATAFILE_MANAGER_FAILED_TO_START, err); + this.logger?.error(SERVICE_FAILED_TO_START, 'DatafileManager', err.message); // If datafile manager onRunning() promise is rejected, and the project config manager // is still in starting state, that means a datafile was not provided in cofig or was invalid, // otherwise the state would have already been set to running synchronously. // In this case, we cannot recover. if (this.isStarting()) { - this.handleInitError(err); + this.handleInitError(new Error( + sprintf(SERVICE_FAILED_TO_START, 'DatafileManager', err.message) + )); } } @@ -173,7 +182,7 @@ export class ProjectConfigManagerImpl extends BaseService implements ProjectConf const fatalError = (this.isStarting() && !this.datafileManager) || (this.isStarting() && !fromConfig); if (fatalError) { - this.handleInitError(err); + this.handleInitError(new Error(GOT_INVALID_DATAFILE)); } } } @@ -206,7 +215,9 @@ export class ProjectConfigManagerImpl extends BaseService implements ProjectConf } if (this.isNew() || this.isStarting()) { - this.startPromise.reject(new OptimizelyError(DATAFILE_MANAGER_STOPPED)); + this.startPromise.reject(new Error( + sprintf(SERVICE_STOPPED_BEFORE_RUNNING, 'ProjectConfigManager') + )); } this.state = ServiceState.Stopping; diff --git a/lib/service.ts b/lib/service.ts index b024ef510..3022aa806 100644 --- a/lib/service.ts +++ b/lib/service.ts @@ -1,5 +1,5 @@ /** - * Copyright 2024 Optimizely + * Copyright 2024-2025 Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,8 @@ import { LoggerFacade, LogLevel, LogLevelToLower } from './logging/logger' import { resolvablePromise, ResolvablePromise } from "./utils/promise/resolvablePromise"; +export const SERVICE_FAILED_TO_START = '%s failed to start, reason: %s'; +export const SERVICE_STOPPED_BEFORE_RUNNING = '%s stopped before running'; /** * The service interface represents an object with an operational state,