Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions packages/experiment-browser/src/config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { FetchHttpClient } from './transport/http';
import { ExperimentAnalyticsProvider } from './types/analytics';
import { ExposureTrackingProvider } from './types/exposure';
import { Logger, LogLevel } from './types/logger';
import { ExperimentUserProvider } from './types/provider';
import { Source } from './types/source';
import { HttpClient } from './types/transport';
Expand All @@ -13,9 +14,23 @@ export interface ExperimentConfig {
/**
* Debug all assignment requests in the UI Debugger and log additional
* information to the console. This should be false for production builds.
* @deprecated Use logLevel instead. When debug is true, it sets logLevel to Debug.
*/
debug?: boolean;

/**
* The minimum log level to output. Messages below this level will be ignored.
* Supported levels: Disable, Error (default), Warn, Info, Debug, Verbose.
* If the deprecated debug flag is set to true, this will default to Debug.
*/
logLevel?: LogLevel;

/**
* Custom logger implementation. If not provided, a default ConsoleLogger will be used.
* The logger must implement the Logger interface with methods for error, warn, info, debug, and verbose.
*/
loggerProvider?: Logger;

/**
* The name of the instance being initialized. Used for initializing separate
* instances of experiment or linking the experiment SDK to a specific
Expand Down Expand Up @@ -165,6 +180,8 @@ export interface ExperimentConfig {
| **Option** | **Default** |
|------------------|-----------------------------------|
| **debug** | `false` |
| **logLevel** | `LogLevel.Error` |
| **logger** | `null` (ConsoleLogger will be used) |
| **instanceName** | `$default_instance` |
| **fallbackVariant** | `null` |
| **initialVariants** | `null` |
Expand All @@ -189,6 +206,8 @@ export interface ExperimentConfig {
*/
export const Defaults: ExperimentConfig = {
debug: false,
logLevel: LogLevel.Error,
loggerProvider: null,
instanceName: '$default_instance',
fallbackVariant: {},
initialVariants: {},
Expand Down
42 changes: 25 additions & 17 deletions packages/experiment-browser/src/experimentClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import { version as PACKAGE_VERSION } from '../package.json';

import { Defaults, ExperimentConfig } from './config';
import { IntegrationManager } from './integration/manager';
import { AmpLogger } from './logger/ampLogger';
import { ConsoleLogger } from './logger/consoleLogger';
import {
getFlagStorage,
getVariantStorage,
Expand All @@ -33,6 +35,7 @@ import { FetchHttpClient, WrapperClient } from './transport/http';
import { exposureEvent } from './types/analytics';
import { Client, FetchOptions } from './types/client';
import { Exposure, ExposureTrackingProvider } from './types/exposure';
import { LogLevel } from './types/logger';
import { ExperimentPlugin, IntegrationPlugin } from './types/plugin';
import { ExperimentUserProvider } from './types/provider';
import { isFallback, Source, VariantSource } from './types/source';
Expand Down Expand Up @@ -75,6 +78,7 @@ const euFlagsServerUrl = 'https://flag.lab.eu.amplitude.com';
export class ExperimentClient implements Client {
private readonly apiKey: string;
private readonly config: ExperimentConfig;
private readonly logger: AmpLogger;
private readonly variants: LoadStoreCache<Variant>;
private readonly flags: LoadStoreCache<EvaluationFlag>;
private readonly flagApi: FlagApi;
Expand Down Expand Up @@ -128,6 +132,10 @@ export class ExperimentClient implements Client {
: config.flagConfigPollingIntervalMillis ??
Defaults.flagConfigPollingIntervalMillis,
};
this.logger = new AmpLogger(
this.config.loggerProvider || new ConsoleLogger(),
ExperimentClient.getLogLevel(config),
);
const internalInstanceName = this.config?.['internalInstanceNameSuffix'];
this.isWebExperiment = internalInstanceName === 'web';
this.poller = new Poller(
Expand Down Expand Up @@ -297,12 +305,10 @@ export class ExperimentClient implements Client {
}

// Otherwise, handle errors silently as before
if (this.config.debug) {
if (e instanceof TimeoutError) {
console.debug(e);
} else {
console.error(e);
}
if (e instanceof TimeoutError) {
this.logger.debug(e);
} else {
this.logger.error(e);
}
}
return this;
Expand Down Expand Up @@ -332,7 +338,7 @@ export class ExperimentClient implements Client {
if (this.config.automaticExposureTracking) {
this.exposureInternal(key, sourceVariant);
}
this.debug(
this.logger.debug(
`[Experiment] variant for ${key} is ${
sourceVariant.variant?.key || sourceVariant.variant?.value
}`,
Expand Down Expand Up @@ -697,7 +703,7 @@ export class ExperimentClient implements Client {
throw Error('Experiment API key is empty');
}

this.debug(`[Experiment] Fetch all: retry=${retry}`);
this.logger.debug(`[Experiment] Fetch all: retry=${retry}`);

// Proactively cancel retries if active in order to avoid unnecessary API
// requests. A new failure will restart the retries.
Expand Down Expand Up @@ -730,7 +736,7 @@ export class ExperimentClient implements Client {
): Promise<Variants> {
user = await this.addContextOrWait(user);
user = this.cleanUserPropsForFetch(user);
this.debug('[Experiment] Fetch variants for user: ', user);
this.logger.debug('[Experiment] Fetch variants for user: ', user);
const results = await this.evaluationApi.getVariants(user, {
timeoutMillis: timeoutMillis,
...options,
Expand All @@ -739,7 +745,7 @@ export class ExperimentClient implements Client {
for (const key of Object.keys(results)) {
variants[key] = convertEvaluationVariantToVariant(results[key]);
}
this.debug('[Experiment] Received variants: ', variants);
this.logger.debug('[Experiment] Received variants: ', variants);
return variants;
}

Expand All @@ -763,7 +769,7 @@ export class ExperimentClient implements Client {
this.flags.putAll(flags);
} catch (e) {
if (e instanceof TimeoutError) {
this.config.debug && console.debug(e);
this.logger.debug(e);
// If throwOnError is configured to true, rethrow timeout errors
if (this.config.throwOnError) {
throw e;
Expand Down Expand Up @@ -802,14 +808,14 @@ export class ExperimentClient implements Client {
} catch (e) {
// catch localStorage undefined error
}
this.debug('[Experiment] Stored variants: ', variants);
this.logger.debug('[Experiment] Stored variants: ', variants);
}

private async startRetries(
user: ExperimentUser,
options: FetchOptions,
): Promise<void> {
this.debug('[Experiment] Retry fetch');
this.logger.debug('[Experiment] Retry fetch');
this.retriesBackoff = new Backoff(
fetchBackoffAttempts,
fetchBackoffMinMillis,
Expand Down Expand Up @@ -935,11 +941,13 @@ export class ExperimentClient implements Client {
}
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
private debug(message?: any, ...optionalParams: any[]): void {
if (this.config.debug) {
console.debug(message, ...optionalParams);
private static getLogLevel(config: ExperimentConfig): LogLevel {
// Backwards compatibility: if debug flag is set to true, use Debug level
if (config.debug === true) {
return LogLevel.Debug;
}
// Otherwise use the configured logLevel or default to Error
return config.logLevel ?? LogLevel.Error;
}

private shouldRetryFetch(e: Error): boolean {
Expand Down
2 changes: 2 additions & 0 deletions packages/experiment-browser/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,5 @@ export {
ExperimentPluginType,
ExperimentEvent,
} from './types/plugin';
export { Logger, LogLevel } from './types/logger';
export { ConsoleLogger } from './logger/consoleLogger';
77 changes: 77 additions & 0 deletions packages/experiment-browser/src/logger/ampLogger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/* eslint-disable @typescript-eslint/no-explicit-any*/
import { Logger, LogLevel } from '../types/logger';

/**
* Internal logger class that wraps a Logger implementation and handles log level filtering.
* This class provides a centralized logging mechanism for the Experiment client.
* @category Logging
*/
export class AmpLogger implements Logger {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also wonder the necessity of the wrapper given we're not doing any filtering or transformation like other SDKs. We usually want to keep experiment-browser lean.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great point about lean skds. I believe this is a low addition way to provide log level filtering on our end as well as have a decent buffer between our log level definitions/usages and others.

private logger: Logger;
private logLevel: LogLevel;

/**
* Creates a new AmpLogger instance
* @param logger The underlying logger implementation to use
* @param logLevel The minimum log level to output. Messages below this level will be ignored.
*/
constructor(logger: Logger, logLevel: LogLevel = LogLevel.Error) {
this.logger = logger;
this.logLevel = logLevel;
}

/**
* Log an error message
* @param message The message to log
* @param optionalParams Additional parameters to log
*/
error(message?: any, ...optionalParams: any[]): void {
if (this.logLevel >= LogLevel.Error) {
this.logger.error(message, ...optionalParams);
}
}

/**
* Log a warning message
* @param message The message to log
* @param optionalParams Additional parameters to log
*/
warn(message?: any, ...optionalParams: any[]): void {
if (this.logLevel >= LogLevel.Warn) {
this.logger.warn(message, ...optionalParams);
}
}

/**
* Log an informational message
* @param message The message to log
* @param optionalParams Additional parameters to log
*/
info(message?: any, ...optionalParams: any[]): void {
if (this.logLevel >= LogLevel.Info) {
this.logger.info(message, ...optionalParams);
}
}

/**
* Log a debug message
* @param message The message to log
* @param optionalParams Additional parameters to log
*/
debug(message?: any, ...optionalParams: any[]): void {
if (this.logLevel >= LogLevel.Debug) {
this.logger.debug(message, ...optionalParams);
}
}

/**
* Log a verbose message
* @param message The message to log
* @param optionalParams Additional parameters to log
*/
verbose(message?: any, ...optionalParams: any[]): void {
if (this.logLevel >= LogLevel.Verbose) {
this.logger.verbose(message, ...optionalParams);
}
}
}
55 changes: 55 additions & 0 deletions packages/experiment-browser/src/logger/consoleLogger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/* eslint-disable no-console,@typescript-eslint/no-explicit-any*/
import { Logger } from '../types/logger';

/**
* Default console-based logger implementation.
* This logger uses the browser's console API to output log messages.
* Log level filtering is handled by the AmpLogger wrapper class.
* @category Logging
*/
export class ConsoleLogger implements Logger {
/**
* Log an error message
* @param message The message to log
* @param optionalParams Additional parameters to log
*/
error(message?: any, ...optionalParams: any[]): void {
console.error(message, ...optionalParams);
}

/**
* Log a warning message
* @param message The message to log
* @param optionalParams Additional parameters to log
*/
warn(message?: any, ...optionalParams: any[]): void {
console.warn(message, ...optionalParams);
}

/**
* Log an informational message
* @param message The message to log
* @param optionalParams Additional parameters to log
*/
info(message?: any, ...optionalParams: any[]): void {
console.info(message, ...optionalParams);
}

/**
* Log a debug message
* @param message The message to log
* @param optionalParams Additional parameters to log
*/
debug(message?: any, ...optionalParams: any[]): void {
console.debug(message, ...optionalParams);
}

/**
* Log a verbose message
* @param message The message to log
* @param optionalParams Additional parameters to log
*/
verbose(message?: any, ...optionalParams: any[]): void {
console.debug(message, ...optionalParams);
}
}
71 changes: 71 additions & 0 deletions packages/experiment-browser/src/types/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/**
* Log level enumeration for controlling logging verbosity.
* @category Logging
*/
export enum LogLevel {
/**
* Disable all logging
*/
Disable = 0,
/**
* Error level logging - only critical errors
*/
Error = 1,
/**
* Warning level logging - errors and warnings
*/
Warn = 2,
/**
* Info level logging - errors, warnings, and informational messages
*/
Info = 3,
/**
* Debug level logging - errors, warnings, info, and debug messages
*/
Debug = 4,
/**
* Verbose level logging - all messages including verbose details
*/
Verbose = 5,
}

/**
* Logger interface that can be implemented to provide custom logging.
* @category Logging
*/
export interface Logger {
/**
* Log an error message
* @param message The message to log
* @param optionalParams Additional parameters to log
*/
error(message?: any, ...optionalParams: any[]): void;

/**
* Log a warning message
* @param message The message to log
* @param optionalParams Additional parameters to log
*/
warn(message?: any, ...optionalParams: any[]): void;

/**
* Log an informational message
* @param message The message to log
* @param optionalParams Additional parameters to log
*/
info(message?: any, ...optionalParams: any[]): void;

/**
* Log a debug message
* @param message The message to log
* @param optionalParams Additional parameters to log
*/
debug(message?: any, ...optionalParams: any[]): void;

/**
* Log a verbose message
* @param message The message to log
* @param optionalParams Additional parameters to log
*/
verbose(message?: any, ...optionalParams: any[]): void;
}
Loading