Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: Resolve Type Warnings for ConfigService.get() #3350

Open
wants to merge 30 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
30ea504
fix: resolve type warnings for ConfigService.get()
belloibrahv Dec 21, 2024
5f696e9
fix: resolve type warnings for ConfigService.get()
belloibrahv Dec 21, 2024
c56d0b2
Refactor GlobalConfig and revert header changes as per feedback
belloibrahv Dec 25, 2024
55fb521
Reverting License Header Changes
belloibrahv Dec 25, 2024
e89a4f8
Refinment of ConfigService.get() (to conform to newly suggested changes)
belloibrahv Dec 26, 2024
e45046a
Refactor ConfigService.get to enhance type safety and remove manual c…
belloibrahv Jan 8, 2025
c50ccbc
fix bugs in workflow
belloibrahv Jan 18, 2025
17baeb5
Remove assesrtion
belloibrahv Jan 21, 2025
1a3e81d
Remove assesrtion
belloibrahv Jan 22, 2025
a8f4fb9
Add assertion to _CONFIG (in the globalConfig.ts file)
belloibrahv Jan 23, 2025
83fa7c3
chore: renamed types and added explanation
quiet-node Jan 30, 2025
0d52b65
fix: loosen strict type checking for array types in config variables
quiet-node Jan 30, 2025
cae9c8b
fix: modified GetTypeOfConfigKey to resolve to better types
quiet-node Jan 30, 2025
bf5a3b8
fix: updated properly default values to all global configs
quiet-node Jan 31, 2025
3cfebcc
fix: fixed ConfigService unit test
quiet-node Jan 31, 2025
e4c3b16
fix: fixed LocalLRUCache suite
quiet-node Jan 31, 2025
98b531e
fix: fixed HbarSpendingPlanConfigService suite
quiet-node Jan 31, 2025
8dc0b31
chore: updated type explanation
quiet-node Jan 31, 2025
f43fb6f
fix: modified typeCasting to always convert CHAIN_ID to hexadecimal v…
quiet-node Jan 31, 2025
ba11374
fix: further tighten ConfigService.get()
quiet-node Feb 1, 2025
80b2e26
Update web3.ts
quiet-node Feb 1, 2025
22ebcfa
fix: fixed test in net
quiet-node Feb 1, 2025
d82d177
fix: fixed test in web3
quiet-node Feb 1, 2025
792e091
fix: fixed eth_feeHistory
quiet-node Feb 1, 2025
a5b4271
fix: fixed filter API
quiet-node Feb 1, 2025
357c68d
fix: fixed HbarLimitService unit test
quiet-node Feb 1, 2025
95380a8
fix: fixed utils.spec.ts
quiet-node Feb 1, 2025
c65a880
fix: fixed BATCH_REQUESTS_ENABLED enabled by default
quiet-node Feb 1, 2025
40912f6
fix: fixed ws-server unit
quiet-node Feb 1, 2025
3a37ec9
chore: switch MULTI_SET to false
quiet-node Feb 1, 2025
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
16 changes: 12 additions & 4 deletions docs/configuration.md

Large diffs are not rendered by default.

1,505 changes: 788 additions & 717 deletions packages/config-service/src/services/globalConfig.ts

Large diffs are not rendered by default.

36 changes: 31 additions & 5 deletions packages/config-service/src/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
import findConfig from 'find-config';
import pino from 'pino';

import type { ConfigKey, GetTypeOfConfigKey } from './globalConfig';
import { GlobalConfig } from './globalConfig';
import { LoggerService } from './loggerService';
import { ValidationService } from './validationService';

Expand Down Expand Up @@ -93,11 +95,35 @@
}

/**
* Get an env var by name
* @param name string
* @returns string | undefined
* Retrieves the value of a specified configuration property using its key name.
*
* The method incorporates validation to ensure required configuration values are provided.
*
* **Note:** The validations in this method, in addition to the `ValidationService.startUp(process.env)` in the constructor, are crucial
* as this method is frequently invoked in testing environments where configuration values might be dynamically
* overridden. Additionally, since this method is the most exposed method across different packages, serving as the
* main gateway for accessing configurations, these validations help strengthen security and prevent undefined
* behavior in both production and testing scenarios.
*
* For the CHAIN_ID key, the value is converted to a hexadecimal format prefixed with '0x'.
*
* @param name - The configuration key to retrieve.
* @typeParam K - The specific type parameter representing the ConfigKey.
* @returns The value associated with the specified key, or the default value from its GlobalConfig entry, properly typed based on the key's configuration.
* @throws Error if a required configuration value is missing.
*/
public static get(name: string): string | number | boolean | null | undefined {
return this.getInstance().envs[name];
public static get<K extends ConfigKey>(name: K): GetTypeOfConfigKey<K> {
const configEntry = GlobalConfig.ENTRIES[name];
let value = this.getInstance().envs[name] == undefined ? configEntry?.defaultValue : this.getInstance().envs[name];

if (value == undefined && configEntry?.required) {
throw new Error(`Configuration error: ${name} is a mandatory configuration for relay operation.`);

Check warning on line 120 in packages/config-service/src/services/index.ts

View check run for this annotation

Codecov / codecov/patch

packages/config-service/src/services/index.ts#L120

Added line #L120 was not covered by tests
}

if (name === 'CHAIN_ID' && value !== undefined) {
value = `0x${Number(value).toString(16)}`;
}

return value as GetTypeOfConfigKey<K>;
}
}
17 changes: 12 additions & 5 deletions packages/config-service/src/services/validationService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,26 +30,33 @@
Object.entries(GlobalConfig.ENTRIES).forEach(([entryName, entryInfo]) => {
if (entryInfo.required) {
if (!envs.hasOwnProperty(entryName)) {
throw new Error(`${entryName} is a mandatory and the relay cannot operate without its value.`);
throw new Error(`Configuration error: ${entryName} is a mandatory configuration for relay operation.`);

Check warning on line 33 in packages/config-service/src/services/validationService.ts

View check run for this annotation

Codecov / codecov/patch

packages/config-service/src/services/validationService.ts#L33

Added line #L33 was not covered by tests
}

if (entryInfo.type === 'number' && isNaN(Number(envs[entryName]))) {
throw new Error(`${entryName} must be a valid number.`);
throw new Error(`Configuration error: ${entryName} must be a valid number.`);

Check warning on line 37 in packages/config-service/src/services/validationService.ts

View check run for this annotation

Codecov / codecov/patch

packages/config-service/src/services/validationService.ts#L37

Added line #L37 was not covered by tests
}
}
});
}

/**
* Transform string env variables to the proper formats (number/boolean/string)
* @param envs
* Transform string environment variables to their proper types based on GlobalConfig.ENTRIES.
* For each entry:
* - If the env var is missing but has a default value, use the default
* - For 'number' type, converts to Number
* - For 'boolean' type, converts 'true' string to true boolean
* - For 'string' and 'array' types, keeps as string
*
* @param envs - Dictionary of environment variables and their string values
* @returns Dictionary with environment variables cast to their proper types
*/
static typeCasting(envs: NodeJS.Dict<string>): NodeJS.Dict<any> {
const typeCastedEnvs: NodeJS.Dict<any> = {};

Object.entries(GlobalConfig.ENTRIES).forEach(([entryName, entryInfo]) => {
if (!envs.hasOwnProperty(entryName)) {
if (entryInfo.defaultValue) {
if (entryInfo.defaultValue != null) {
typeCastedEnvs[entryName] = entryInfo.defaultValue;
}
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import chai, { expect } from 'chai';
import chaiAsPromised from 'chai-as-promised';
import { ConfigService } from '../../../src/services';
import { GlobalConfig, type ConfigKey } from '../../../src/services/globalConfig';

chai.use(chaiAsPromised);

Expand Down Expand Up @@ -50,13 +51,77 @@ describe('ConfigService tests', async function () {

it('should be able to get existing env var', async () => {
const res = ConfigService.get('CHAIN_ID');

expect(res).to.equal('0x12a');
});

it('should return undefined for non-existing variable', async () => {
const res = ConfigService.get('NON_EXISTING_VAR');

const res = ConfigService.get('NON_EXISTING_VAR' as ConfigKey);
acuarica marked this conversation as resolved.
Show resolved Hide resolved
expect(res).to.equal(undefined);
});

it('should return the default value for configurations not set in process.env', async () => {
const targetKey = 'FILE_APPEND_MAX_CHUNKS';
const envValue = process.env[targetKey];

// ensure the key is not listed in env
expect(envValue).to.be.undefined;

const expectedDefaultValue = GlobalConfig.ENTRIES[targetKey].defaultValue;

const res = ConfigService.get(targetKey);
expect(res).to.equal(expectedDefaultValue);
});

it('should infer the explicit type for configuration which is either required or has a valid defaultValue', () => {
const targetKeys = [
'FILE_APPEND_MAX_CHUNKS',
'GET_RECORD_DEFAULT_TO_CONSENSUS_NODE',
'E2E_RELAY_HOST',
'ETH_CALL_ACCEPTED_ERRORS',
] as ConfigKey[];

targetKeys.forEach((targetKey) => {
const result = ConfigService.get(targetKey);
const expectedTypeString = GlobalConfig.ENTRIES[targetKey].type;

switch (expectedTypeString) {
case 'number':
expect(typeof result === 'number').to.be.true;
break;
case 'boolean':
expect(typeof result === 'boolean').to.be.true;
break;
case 'string':
case 'array':
expect(typeof result === 'string').to.be.true;
break;
}
});
});

it('Should always convert CHAIN_ID to a hexadecimal string, regardless of input value type.', async () => {
const originalEnv = process.env;

const testChainId = (input: string, expected: string) => {
process.env = { ...originalEnv, CHAIN_ID: input };
// Reset the ConfigService singleton instance to force a new initialization
// This is necessary because ConfigService caches the env values when first instantiated,
// so we need to clear that cache to test with our new CHAIN_ID value
// @ts-ignore - accessing private property for testing
delete ConfigService.instance;
expect(ConfigService.get('CHAIN_ID')).to.equal(expected);
};

try {
// Test cases
testChainId('298', '0x12a'); // decimal number
testChainId('0x12a', '0x12a'); // hexadecimal with prefix
testChainId('1000000', '0xf4240'); // larger number
testChainId('0xhedera', '0xNaN'); // invalid number
} finally {
process.env = originalEnv;
// @ts-ignore - accessing private property for testing
delete ConfigService.instance;
}
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ describe('LoggerService tests', async function () {
});

it('should be able to return plain information', async () => {
const envName = GlobalConfig.ENTRIES.CHAIN_ID.envName;
const envName = 'CHAIN_ID';
const res = ConfigService.get(envName);

expect(LoggerService.maskUpEnv(envName, res)).to.equal(`${envName} = ${res}`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ describe('ValidationService tests', async function () {

it('should fail fast if mandatory env is not passed', async () => {
expect(() => ValidationService.startUp({})).to.throw(
'CHAIN_ID is a mandatory and the relay cannot operate without its value.',
'Configuration error: CHAIN_ID is a mandatory configuration for relay operation.',
);
});

Expand Down Expand Up @@ -77,7 +77,7 @@ describe('ValidationService tests', async function () {
ValidationService.startUp({
...mandatoryStartUpFields,
}),
).to.throw('npm_package_version is a mandatory and the relay cannot operate without its value.');
).to.throw('Configuration error: npm_package_version is a mandatory configuration for relay operation.');
});
});

Expand All @@ -92,8 +92,8 @@ describe('ValidationService tests', async function () {

it('should skip adding value if it is missing and there is no default value set', async () => {
const castedEnvs = ValidationService.typeCasting({});
expect(castedEnvs).to.not.haveOwnProperty(GlobalConfig.ENTRIES.FILTER_TTL.envName);
expect(castedEnvs[GlobalConfig.ENTRIES.FILTER_TTL.envName]).to.be.undefined;
expect(castedEnvs).to.not.haveOwnProperty(GlobalConfig.ENTRIES.GH_ACCESS_TOKEN.envName);
expect(castedEnvs[GlobalConfig.ENTRIES.GH_ACCESS_TOKEN.envName]).to.be.undefined;
});

it('should to cast string type', async () => {
Expand Down
7 changes: 2 additions & 5 deletions packages/relay/src/lib/clients/cache/localLRUCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
import { Logger } from 'pino';
import { Gauge, Registry } from 'prom-client';
import { ICacheClient } from './ICacheClient';
import constants from '../../constants';
import LRUCache, { LimitedByCount, LimitedByTTL } from 'lru-cache';
import { ConfigService } from '@hashgraph/json-rpc-config-service/dist/services';
import { RequestDetails } from '../../types';
Expand All @@ -40,11 +39,9 @@ export class LocalLRUCache implements ICacheClient {
*/
private readonly options: LimitedByCount & LimitedByTTL = {
// The maximum number (or size) of items that remain in the cache (assuming no TTL pruning or explicit deletions).
// @ts-ignore
max: Number.parseInt(ConfigService.get('CACHE_MAX') ?? constants.CACHE_MAX.toString()),
max: ConfigService.get('CACHE_MAX'),
// Max time to live in ms, for items before they are considered stale.
// @ts-ignore
ttl: Number.parseInt(ConfigService.get('CACHE_TTL') ?? constants.CACHE_TTL.ONE_HOUR.toString()),
ttl: ConfigService.get('CACHE_TTL'),
};

/**
Expand Down
6 changes: 2 additions & 4 deletions packages/relay/src/lib/clients/cache/redisCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,7 @@ export class RedisCache implements IRedisCacheClient {
*/
private readonly options = {
// Max time to live in ms, for items before they are considered stale.
// @ts-ignore
ttl: Number.parseInt(ConfigService.get('CACHE_TTL') ?? constants.CACHE_TTL.ONE_HOUR.toString()),
ttl: ConfigService.get('CACHE_TTL'),
};

/**
Expand Down Expand Up @@ -79,8 +78,7 @@ export class RedisCache implements IRedisCacheClient {
this.register = register;

const redisUrl = ConfigService.get('REDIS_URL')!;
// @ts-ignore
const reconnectDelay = parseInt(ConfigService.get('REDIS_RECONNECT_DELAY_MS') || '1000');
const reconnectDelay = ConfigService.get('REDIS_RECONNECT_DELAY_MS');
this.client = createClient({
// @ts-ignore
url: redisUrl,
Expand Down
54 changes: 18 additions & 36 deletions packages/relay/src/lib/clients/mirrorNodeClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,47 +161,29 @@ export class MirrorNodeClient {

static readonly EVM_ADDRESS_REGEX: RegExp = /\/accounts\/([\d\.]+)/;

public static readonly mirrorNodeContractResultsPageMax = parseInt(
// @ts-ignore
ConfigService.get('MIRROR_NODE_CONTRACT_RESULTS_PG_MAX') || 25,
public static readonly mirrorNodeContractResultsPageMax = ConfigService.get('MIRROR_NODE_CONTRACT_RESULTS_PG_MAX');
public static readonly mirrorNodeContractResultsLogsPageMax = ConfigService.get(
'MIRROR_NODE_CONTRACT_RESULTS_LOGS_PG_MAX',
);
public static readonly mirrorNodeContractResultsLogsPageMax =
// @ts-ignore
parseInt(ConfigService.get('MIRROR_NODE_CONTRACT_RESULTS_LOGS_PG_MAX') || 200);

protected createAxiosClient(baseUrl: string): AxiosInstance {
// defualt values for axios clients to mirror node
// @ts-ignore
const mirrorNodeTimeout = parseInt(ConfigService.get('MIRROR_NODE_TIMEOUT') || '10000');
// @ts-ignore
const mirrorNodeMaxRedirects = parseInt(ConfigService.get('MIRROR_NODE_MAX_REDIRECTS') || '5');
const mirrorNodeTimeout = ConfigService.get('MIRROR_NODE_TIMEOUT');
const mirrorNodeMaxRedirects = ConfigService.get('MIRROR_NODE_MAX_REDIRECTS');
const mirrorNodeHttpKeepAlive = ConfigService.get('MIRROR_NODE_HTTP_KEEP_ALIVE');
// @ts-ignore
const mirrorNodeHttpKeepAliveMsecs = parseInt(ConfigService.get('MIRROR_NODE_HTTP_KEEP_ALIVE_MSECS') || '1000');
// @ts-ignore
const mirrorNodeHttpMaxSockets = parseInt(ConfigService.get('MIRROR_NODE_HTTP_MAX_SOCKETS') || '300');
// @ts-ignore
const mirrorNodeHttpMaxTotalSockets = parseInt(ConfigService.get('MIRROR_NODE_HTTP_MAX_TOTAL_SOCKETS') || '300');
// @ts-ignore
const mirrorNodeHttpSocketTimeout = parseInt(ConfigService.get('MIRROR_NODE_HTTP_SOCKET_TIMEOUT') || '60000');
const mirrorNodeHttpKeepAliveMsecs = ConfigService.get('MIRROR_NODE_HTTP_KEEP_ALIVE_MSECS');
const mirrorNodeHttpMaxSockets = ConfigService.get('MIRROR_NODE_HTTP_MAX_SOCKETS');
const mirrorNodeHttpMaxTotalSockets = ConfigService.get('MIRROR_NODE_HTTP_MAX_TOTAL_SOCKETS');
const mirrorNodeHttpSocketTimeout = ConfigService.get('MIRROR_NODE_HTTP_SOCKET_TIMEOUT');
const isDevMode = ConfigService.get('DEV_MODE');
// @ts-ignore
const mirrorNodeRetries = parseInt(ConfigService.get('MIRROR_NODE_RETRIES') || '0'); // we are in the process of deprecating this feature
// @ts-ignore
const mirrorNodeRetriesDevMode = parseInt(ConfigService.get('MIRROR_NODE_RETRIES_DEVMODE') || '5');
const mirrorNodeRetries = ConfigService.get('MIRROR_NODE_RETRIES'); // we are in the process of deprecating this feature
const mirrorNodeRetriesDevMode = ConfigService.get('MIRROR_NODE_RETRIES_DEVMODE');
const mirrorNodeRetryDelay = this.MIRROR_NODE_RETRY_DELAY;
// @ts-ignore
const mirrorNodeRetryDelayDevMode = parseInt(ConfigService.get('MIRROR_NODE_RETRY_DELAY_DEVMODE') || '200');
const mirrorNodeRetryErrorCodes = ConfigService.get('MIRROR_NODE_RETRY_CODES')
? // @ts-ignore
JSON.parse(ConfigService.get('MIRROR_NODE_RETRY_CODES'))
: []; // we are in the process of deprecating this feature
// by default will be true, unless explicitly set to false.
// @ts-ignore
const mirrorNodeRetryDelayDevMode = ConfigService.get('MIRROR_NODE_RETRY_DELAY_DEVMODE');
const mirrorNodeRetryErrorCodes = JSON.parse(ConfigService.get('MIRROR_NODE_RETRY_CODES')); // we are in the process of deprecating this feature by default will be true, unless explicitly set to false.
const useCacheableDnsLookup: boolean = ConfigService.get('MIRROR_NODE_AGENT_CACHEABLE_DNS');

const httpAgent = new http.Agent({
// @ts-ignore
keepAlive: mirrorNodeHttpKeepAlive,
keepAliveMsecs: mirrorNodeHttpKeepAliveMsecs,
maxSockets: mirrorNodeHttpMaxSockets,
Expand Down Expand Up @@ -258,6 +240,7 @@ export class MirrorNodeClient {
return delay;
},
retryCondition: (error) => {
// @ts-ignore
return !error?.response?.status || mirrorNodeRetryErrorCodes.includes(error?.response?.status);
},
shouldResetTimeout: true,
Expand Down Expand Up @@ -313,11 +296,11 @@ export class MirrorNodeClient {
this.cacheService = cacheService;

// set up eth call accepted error codes.
if (ConfigService.get('ETH_CALL_ACCEPTED_ERRORS')) {
const parsedAcceptedError = JSON.parse(ConfigService.get('ETH_CALL_ACCEPTED_ERRORS'));
if (parsedAcceptedError.length != 0) {
MirrorNodeClient.acceptedErrorStatusesResponsePerRequestPathMap.set(
MirrorNodeClient.CONTRACT_CALL_ENDPOINT,
// @ts-ignore
JSON.parse(ConfigService.get('ETH_CALL_ACCEPTED_ERRORS')),
parsedAcceptedError,
);
}
}
Expand Down Expand Up @@ -1334,8 +1317,7 @@ export class MirrorNodeClient {
this.setQueryParam(queryParamObject, 'limit', limitOrderParams.limit);
this.setQueryParam(queryParamObject, 'order', limitOrderParams.order);
} else {
// @ts-ignore
this.setQueryParam(queryParamObject, 'limit', parseInt(ConfigService.get('MIRROR_NODE_LIMIT_PARAM') || '100'));
this.setQueryParam(queryParamObject, 'limit', ConfigService.get('MIRROR_NODE_LIMIT_PARAM'));
this.setQueryParam(queryParamObject, 'order', constants.ORDER.ASC);
}
}
Expand Down
15 changes: 6 additions & 9 deletions packages/relay/src/lib/clients/sdkClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,18 +140,16 @@
) {
this.clientMain = clientMain;

if (ConfigService.get('CONSENSUS_MAX_EXECUTION_TIME')) {
// sets the maximum time in ms for the SDK to wait when submitting
// a transaction/query before throwing a TIMEOUT error
this.clientMain = clientMain.setMaxExecutionTime(Number(ConfigService.get('CONSENSUS_MAX_EXECUTION_TIME')));
}
// sets the maximum time in ms for the SDK to wait when submitting
// a transaction/query before throwing a TIMEOUT error
this.clientMain = clientMain.setMaxExecutionTime(ConfigService.get('CONSENSUS_MAX_EXECUTION_TIME'));

this.logger = logger;
this.cacheService = cacheService;
this.eventEmitter = eventEmitter;
this.hbarLimitService = hbarLimitService;
this.maxChunks = Number(ConfigService.get('FILE_APPEND_MAX_CHUNKS')) || 20;
this.fileAppendChunkSize = Number(ConfigService.get('FILE_APPEND_CHUNK_SIZE')) || 5120;
this.maxChunks = ConfigService.get('FILE_APPEND_MAX_CHUNKS');
this.fileAppendChunkSize = ConfigService.get('FILE_APPEND_CHUNK_SIZE');
}

/**
Expand Down Expand Up @@ -519,8 +517,7 @@
): Promise<ContractFunctionResult> {
let retries = 0;
let resp;
// @ts-ignore
while (parseInt(ConfigService.get('CONTRACT_QUERY_TIMEOUT_RETRIES') || '1') > retries) {
while (ConfigService.get('CONTRACT_QUERY_TIMEOUT_RETRIES') > retries) {

Check warning on line 520 in packages/relay/src/lib/clients/sdkClient.ts

View check run for this annotation

Codecov / codecov/patch

packages/relay/src/lib/clients/sdkClient.ts#L520

Added line #L520 was not covered by tests
try {
resp = await this.submitContractCallQuery(to, data, gas, from, callerName, requestDetails);
return resp;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ export class HbarSpendingPlanConfigService {
* @private
*/
private static loadSpendingPlansConfig(logger: Logger): SpendingPlanConfig[] {
const spendingPlanConfig = ConfigService.get('HBAR_SPENDING_PLANS_CONFIG') as string;
const spendingPlanConfig = ConfigService.get('HBAR_SPENDING_PLANS_CONFIG');

if (!spendingPlanConfig) {
if (logger.isLevelEnabled('trace')) {
Expand Down
Loading
Loading