From 4becbd0d05ad494c36b8bcc9c570add7c87aa721 Mon Sep 17 00:00:00 2001 From: AnastasiiaSvietlova Date: Mon, 9 Jun 2025 13:10:42 +0200 Subject: [PATCH 1/2] Convert feature-flag-delegate to TypeScript --- packages/core/package.json | 3 +- packages/core/src/client.ts | 12 +-- packages/core/src/event.ts | 2 +- packages/core/src/index.ts | 1 + .../core/src/lib/feature-flag-delegate.js | 70 --------------- .../core/src/lib/feature-flag-delegate.ts | 87 +++++++++++++++++++ .../test/lib/feature-flag-delegate.test.ts | 44 +++++----- .../client-state-persistence.js | 2 +- .../deliver-minidumps.js | 2 +- .../plugin-electron-ipc/bugsnag-ipc-main.js | 3 +- .../renderer-event-data.js | 2 +- 11 files changed, 122 insertions(+), 106 deletions(-) delete mode 100644 packages/core/src/lib/feature-flag-delegate.js create mode 100644 packages/core/src/lib/feature-flag-delegate.ts diff --git a/packages/core/package.json b/packages/core/package.json index dc1ddc96d4..729cd9698b 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -15,8 +15,7 @@ "./lib/es-utils/keys": "./src/lib/es-utils/keys.js", "./lib/derecursify": "./src/lib/derecursify.js", "./lib/callback-runner": "./src/lib/callback-runner.js", - "./lib/path-normalizer": "./src/lib/path-normalizer.js", - "./lib/feature-flag-delegate": "./src/lib/feature-flag-delegate.js" + "./lib/path-normalizer": "./src/lib/path-normalizer.js" }, "description": "Core classes and utilities for Bugsnag notifiers", "homepage": "https://www.bugsnag.com/", diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts index 7f075682be..137349d66a 100644 --- a/packages/core/src/client.ts +++ b/packages/core/src/client.ts @@ -9,7 +9,7 @@ import runCallbacks from './lib/callback-runner' import metadataDelegate from './lib/metadata-delegate' import runSyncCallbacks from './lib/sync-callback-runner' import BREADCRUMB_TYPES from './lib/breadcrumb-types' -import { add, clear, merge } from './lib/feature-flag-delegate' +import featureFlagDelegate from './lib/feature-flag-delegate' import { BreadcrumbType, Config, Delivery, FeatureFlag, LoggerConfig, NotifiableError, Notifier, OnBreadcrumbCallback, OnErrorCallback, OnSessionCallback, Plugin, SessionDelegate, User } from './common' const HUB_PREFIX = '00000' @@ -128,15 +128,15 @@ export default class Client { } addFeatureFlag (name: string, variant: string | null = null) { - add(this._features, this._featuresIndex, name, variant) + featureFlagDelegate.add(this._features, this._featuresIndex, name, variant) } addFeatureFlags (featureFlags: FeatureFlag[]) { - merge(this._features, featureFlags, this._featuresIndex) + featureFlagDelegate.merge(this._features, featureFlags, this._featuresIndex) } clearFeatureFlag (name: string) { - clear(this._features, this._featuresIndex, name) + featureFlagDelegate.clear(this._features, this._featuresIndex, name) } clearFeatureFlags () { @@ -206,7 +206,7 @@ export default class Client { // update and elevate some options this._metadata = assign({}, config.metadata) - merge(this._features, config.featureFlags, this._featuresIndex) + featureFlagDelegate.merge(this._features, config.featureFlags, this._featuresIndex) this._user = assign({}, config.user) this._context = config.context if (config.logger) this._logger = config.logger @@ -352,7 +352,7 @@ export default class Client { event._metadata = assign({}, event._metadata, this._metadata) event._user = assign({}, event._user, this._user) event.breadcrumbs = this._breadcrumbs.slice() - merge(event._features, this._features, event._featuresIndex) + featureFlagDelegate.merge(event._features, this._features, event._featuresIndex) // exit early if events should not be sent on the current releaseStage if (this._config.enabledReleaseStages !== null && !includes(this._config.enabledReleaseStages, this._config.releaseStage)) { diff --git a/packages/core/src/event.ts b/packages/core/src/event.ts index 00e665998c..43d8fc6e26 100644 --- a/packages/core/src/event.ts +++ b/packages/core/src/event.ts @@ -34,7 +34,7 @@ export default class Event { public threads: Thread[] public _metadata: { [key: string]: any } - public _features: FeatureFlag | null[] + public _features: (FeatureFlag | null)[] public _featuresIndex: { [key: string]: number } public _user: User diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 13e5585d59..d88c8baf72 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -12,6 +12,7 @@ export { default as listOfFunctions } from './lib/validators/list-of-functions' export { default as stringWithLength } from './lib/validators/string-with-length' export { default as metadataDelegate } from './lib/metadata-delegate' export { default as nodeFallbackStack } from './lib/node-fallback-stack' +export { default as featureFlagDelegate } from './lib/feature-flag-delegate' export { default as runSyncCallbacks } from './lib/sync-callback-runner' export * from './common' diff --git a/packages/core/src/lib/feature-flag-delegate.js b/packages/core/src/lib/feature-flag-delegate.js deleted file mode 100644 index 0e957610e0..0000000000 --- a/packages/core/src/lib/feature-flag-delegate.js +++ /dev/null @@ -1,70 +0,0 @@ -const isArray = require('./es-utils/is-array') -const jsonStringify = require('@bugsnag/safe-json-stringify') - -function add (existingFeatures, existingFeatureKeys, name, variant) { - if (typeof name !== 'string') { - return - } - - if (variant === undefined) { - variant = null - } else if (variant !== null && typeof variant !== 'string') { - variant = jsonStringify(variant) - } - - const existingIndex = existingFeatureKeys[name] - if (typeof existingIndex === 'number') { - existingFeatures[existingIndex] = { name, variant } - return - } - - existingFeatures.push({ name, variant }) - existingFeatureKeys[name] = existingFeatures.length - 1 -} - -function merge (existingFeatures, newFeatures, existingFeatureKeys) { - if (!isArray(newFeatures)) { - return - } - - for (let i = 0; i < newFeatures.length; ++i) { - const feature = newFeatures[i] - - if (feature === null || typeof feature !== 'object') { - continue - } - - // 'add' will handle if 'name' doesn't exist & 'variant' is optional - add(existingFeatures, existingFeatureKeys, feature.name, feature.variant) - } - - return existingFeatures -} - -// convert feature flags from a map of 'name -> variant' into the format required -// by the Bugsnag Event API: -// [{ featureFlag: 'name', variant: 'variant' }, { featureFlag: 'name 2' }] -function toEventApi (featureFlags) { - return featureFlags.filter(Boolean).map( - ({ name, variant }) => { - const flag = { featureFlag: name } - - // don't add a 'variant' property unless there's actually a value - if (typeof variant === 'string') { - flag.variant = variant - } - - return flag - } - ) -} - -function clear (features, featuresIndex, name) { - const existingIndex = featuresIndex[name] - if (typeof existingIndex === 'number') { - features[existingIndex] = null - delete featuresIndex[name] - } -} - -module.exports = { add, clear, merge, toEventApi } diff --git a/packages/core/src/lib/feature-flag-delegate.ts b/packages/core/src/lib/feature-flag-delegate.ts new file mode 100644 index 0000000000..e03a5a4a80 --- /dev/null +++ b/packages/core/src/lib/feature-flag-delegate.ts @@ -0,0 +1,87 @@ +import isArray from './es-utils/is-array' +import jsonStringify from '@bugsnag/safe-json-stringify' +import {FeatureFlag} from "../common"; + +type FeatureFlagEventApi = { + featureFlag: string + variant?: string +} + +interface FeatureFlagDelegate{ + add: (existingFeatures: Array, existingFeatureKeys: { [key: string]: number }, name?: string | null, variant?: any ) => void + merge: ( + existingFeatures: Array<{ name: string; variant?: any } | null>, + newFeatures: any, + existingFeatureKeys: { [key: string]: any } + ) => Array<{ name: string; variant: any }> | undefined + toEventApi: (featureFlags: Array) => FeatureFlagEventApi[] + clear: (features: (FeatureFlag | null)[], featuresIndex: { [key: string]: number }, name: string) => void +} + +const featureFlagDelegate: FeatureFlagDelegate = { + add: (existingFeatures, existingFeatureKeys, name, variant) => { + if (typeof name !== 'string') { + return + } + + if (variant === undefined) { + variant = null + } else if (variant !== null && typeof variant !== 'string') { + variant = jsonStringify(variant) + } + + const existingIndex = existingFeatureKeys[name] + if (typeof existingIndex === 'number') { + existingFeatures[existingIndex] = {name, variant} + return + } + + existingFeatures.push({name, variant}) + existingFeatureKeys[name] = existingFeatures.length - 1 + }, + + merge: (existingFeatures, newFeatures, existingFeatureKeys) => { + if (!isArray(newFeatures)) { + return + } + + for (let i = 0; i < newFeatures.length; ++i) { + const feature = newFeatures[i] + + if (feature === null || typeof feature !== 'object') { + continue + } + + // 'add' will handle if 'name' doesn't exist & 'variant' is optional + featureFlagDelegate.add(existingFeatures, existingFeatureKeys, feature.name, feature.variant) + } + + // Remove any nulls from the array to match the return type + return existingFeatures.filter(f => f) as Array<{ name: string; variant: any }> + }, + +// convert feature flags from a map of 'name -> variant' into the format required +// by the Bugsnag Event API: +// [{ featureFlag: 'name', variant: 'variant' }, { featureFlag: 'name 2' }] + toEventApi: (featureFlags) => { + return (featureFlags || []) + .filter((flag): flag is FeatureFlag => flag !== null && typeof flag === 'object') + .map((flag) => { + const result: FeatureFlagEventApi = { featureFlag: flag.name }; + if (typeof flag.variant === 'string') { + result.variant = flag.variant; + } + return result; + }); + }, + clear: (features, featuresIndex, name) => { + const existingIndex = featuresIndex[name] + if (typeof existingIndex === 'number') { + features[existingIndex] = null + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete featuresIndex[name] + } + } +} + +export default featureFlagDelegate \ No newline at end of file diff --git a/packages/core/test/lib/feature-flag-delegate.test.ts b/packages/core/test/lib/feature-flag-delegate.test.ts index 7164f82488..ac2f34454f 100644 --- a/packages/core/test/lib/feature-flag-delegate.test.ts +++ b/packages/core/test/lib/feature-flag-delegate.test.ts @@ -1,12 +1,12 @@ -import delegate from '../../src/lib/feature-flag-delegate' +import featureFlagDelegate from '../../src/lib/feature-flag-delegate' -describe('feature flag delegate', () => { +describe('feature flag featureFlagDelegate', () => { describe('#add', () => { it('should do nothing if name is not passed', () => { const existingFeatures = [{ name: 'abc', variant: 'xyz' }] const existingFeaturesIndex = { abc: 0 } - delegate.add(existingFeatures, existingFeaturesIndex) + featureFlagDelegate.add(existingFeatures, existingFeaturesIndex) expect(existingFeatures).toStrictEqual([{ name: 'abc', variant: 'xyz' }]) expect(existingFeaturesIndex).toStrictEqual({ abc: 0 }) @@ -16,7 +16,7 @@ describe('feature flag delegate', () => { const existingFeatures = [{ name: 'abc', variant: 'xyz' }] const existingFeaturesIndex = { abc: 0 } - delegate.add(existingFeatures, existingFeaturesIndex, undefined, '???') + featureFlagDelegate.add(existingFeatures, existingFeaturesIndex, undefined, '???') expect(existingFeatures).toStrictEqual([{ name: 'abc', variant: 'xyz' }]) expect(existingFeaturesIndex).toStrictEqual({ abc: 0 }) @@ -26,7 +26,7 @@ describe('feature flag delegate', () => { const existingFeatures = [{ name: 'abc', variant: 'xyz' }] const existingFeaturesIndex = { abc: 0 } - delegate.add(existingFeatures, existingFeaturesIndex, null, '???') + featureFlagDelegate.add(existingFeatures, existingFeaturesIndex, null, '???') expect(existingFeatures).toStrictEqual([{ name: 'abc', variant: 'xyz' }]) expect(existingFeaturesIndex).toStrictEqual({ abc: 0 }) @@ -36,7 +36,7 @@ describe('feature flag delegate', () => { const existingFeatures: any[] = [] const existingFeaturesIndex = {} - delegate.add(existingFeatures, existingFeaturesIndex, 'good_feature') + featureFlagDelegate.add(existingFeatures, existingFeaturesIndex, 'good_feature') expect(existingFeatures).toStrictEqual([{ name: 'good_feature', variant: null }]) expect(existingFeaturesIndex).toStrictEqual({ good_feature: 0 }) @@ -46,7 +46,7 @@ describe('feature flag delegate', () => { const existingFeatures: [] = [] const existingFeaturesIndex = {} - delegate.add(existingFeatures, existingFeaturesIndex, 'ok_feature', null) + featureFlagDelegate.add(existingFeatures, existingFeaturesIndex, 'ok_feature', null) expect(existingFeatures).toStrictEqual([{ name: 'ok_feature', variant: null }]) expect(existingFeaturesIndex).toStrictEqual({ ok_feature: 0 }) @@ -56,7 +56,7 @@ describe('feature flag delegate', () => { const existingFeatures: any[] = [] const existingFeaturesIndex = {} - delegate.add(existingFeatures, existingFeaturesIndex, 'cool_feature', 'very ant') + featureFlagDelegate.add(existingFeatures, existingFeaturesIndex, 'cool_feature', 'very ant') expect(existingFeatures).toStrictEqual([{ name: 'cool_feature', variant: 'very ant' }]) expect(existingFeaturesIndex).toStrictEqual({ cool_feature: 0 }) @@ -75,7 +75,7 @@ describe('feature flag delegate', () => { const existingFeatures: any[] = [] const existingFeaturesIndex = {} - delegate.add(existingFeatures, existingFeaturesIndex, 'some_feature', variant) + featureFlagDelegate.add(existingFeatures, existingFeaturesIndex, 'some_feature', variant) expect(existingFeatures).toStrictEqual([{ name: 'some_feature', variant: expected }]) expect(existingFeaturesIndex).toStrictEqual({ some_feature: 0 }) @@ -85,9 +85,9 @@ describe('feature flag delegate', () => { const existingFeatures = [{ name: 'a', variant: 'a' }, { name: 'b', variant: 'b' }, { name: 'c', variant: 'c' }, { name: 'd', variant: 'd' }, { name: 'e', variant: 'e' }] const existingFeaturesIndex = { a: 0, b: 1, c: 2, d: 3, e: 4 } - delegate.add(existingFeatures, existingFeaturesIndex, 'b', 'x') - delegate.add(existingFeatures, existingFeaturesIndex, 'e', 'y') - delegate.add(existingFeatures, existingFeaturesIndex, 'a', 'z') + featureFlagDelegate.add(existingFeatures, existingFeaturesIndex, 'b', 'x') + featureFlagDelegate.add(existingFeatures, existingFeaturesIndex, 'e', 'y') + featureFlagDelegate.add(existingFeatures, existingFeaturesIndex, 'a', 'z') expect(existingFeatures).toStrictEqual([{ name: 'a', variant: 'z' }, { name: 'b', variant: 'x' }, { name: 'c', variant: 'c' }, { name: 'd', variant: 'd' }, { name: 'e', variant: 'y' }]) expect(existingFeaturesIndex).toStrictEqual({ a: 0, b: 1, c: 2, d: 3, e: 4 }) @@ -99,7 +99,7 @@ describe('feature flag delegate', () => { const existingFeatures: any[] = [] const existingFeaturesIndex = {} - delegate.merge(existingFeatures, [ + featureFlagDelegate.merge(existingFeatures, [ { name: 'a', variant: 'b' }, { name: 'c', variant: 'd' } ], existingFeaturesIndex) @@ -112,7 +112,7 @@ describe('feature flag delegate', () => { const existingFeatures = [{ name: 'a', variant: 'a' }, { name: 'b', variant: 'b' }, { name: 'c', variant: 'c' }, { name: 'd', variant: 'd' }, { name: 'e', variant: 'e' }] const existingFeaturesIndex = { a: 0, b: 1, c: 2, d: 3, e: 4 } - delegate.merge(existingFeatures, [ + featureFlagDelegate.merge(existingFeatures, [ { name: 'b', variant: 'x' }, { name: 'e', variant: 'y' }, { name: 'a', variant: 'z' } @@ -143,7 +143,7 @@ describe('feature flag delegate', () => { { name: 'p', variant: 'p' } ] - delegate.merge(existingFeatures, [ + featureFlagDelegate.merge(existingFeatures, [ { name: 'a', variant: 12345 }, { name: 'c', variant: 0 }, { name: 'e', variant: true }, @@ -183,7 +183,7 @@ describe('feature flag delegate', () => { const existingFeatures = [{ name: 'a', variant: 'a' }, { name: 'b', variant: 'b' }, { name: 'c', variant: 'c' }, { name: 'd', variant: 'd' }, { name: 'e', variant: 'e' }] const existingFeaturesIndex = { a: 0, b: 1, c: 2, d: 3, e: 3 } - delegate.merge(existingFeatures, [ + featureFlagDelegate.merge(existingFeatures, [ { name: 'b', variant: 'x' }, { variant: 'y' }, { name: 'a', variant: 'z' }, @@ -200,7 +200,7 @@ describe('feature flag delegate', () => { const existingFeatures = [{ name: 'a', variant: 'a' }, { name: 'b', variant: 'b' }, { name: 'c', variant: 'c' }, { name: 'd', variant: 'd' }, { name: 'e', variant: 'e' }] const existingFeaturesIndex = { a: 'a', b: 'b', c: 'c', d: 'd', e: 'e' } - delegate.merge(existingFeatures, { a: 'a', b: 'b', c: 'c' }, existingFeaturesIndex) + featureFlagDelegate.merge(existingFeatures, { a: 'a', b: 'b', c: 'c' }, existingFeaturesIndex) expect(existingFeatures).toStrictEqual([{ name: 'a', variant: 'a' }, { name: 'b', variant: 'b' }, { name: 'c', variant: 'c' }, { name: 'd', variant: 'd' }, { name: 'e', variant: 'e' }]) expect(existingFeaturesIndex).toStrictEqual({ a: 'a', b: 'b', c: 'c', d: 'd', e: 'e' }) @@ -210,7 +210,7 @@ describe('feature flag delegate', () => { const features = [{ name: 'a', variant: 'a' }, { name: 'b', variant: 'b' }, { name: 'c', variant: 'c' }, { name: 'd', variant: 'd' }, { name: 'e', variant: 'e' }] const featuresIndex = { a: 0, b: 1, c: 2, d: 3, e: 4 } - delegate.merge(features, [ + featureFlagDelegate.merge(features, [ { name: 'b', variant: 'x' }, 'name: yes', undefined, @@ -230,8 +230,8 @@ describe('feature flag delegate', () => { const features: any[] = [] const featuresIndex = {} - delegate.add(features, featuresIndex, 'a', 'b') - delegate.merge(features, [ + featureFlagDelegate.add(features, featuresIndex, 'a', 'b') + featureFlagDelegate.merge(features, [ { name: 'c', variant: 'd' }, { name: 'e' }, { name: 'f', variant: 'g' } @@ -239,7 +239,7 @@ describe('feature flag delegate', () => { expect(features).toStrictEqual([{ name: 'a', variant: 'b' }, { name: 'c', variant: 'd' }, { name: 'e', variant: null }, { name: 'f', variant: 'g' }]) - expect(delegate.toEventApi(features)).toStrictEqual([ + expect(featureFlagDelegate.toEventApi(features)).toStrictEqual([ { featureFlag: 'a', variant: 'b' }, { featureFlag: 'c', variant: 'd' }, { featureFlag: 'e' }, @@ -248,7 +248,7 @@ describe('feature flag delegate', () => { }) it('should handle an empty array', () => { - expect(delegate.toEventApi([])).toStrictEqual([]) + expect(featureFlagDelegate.toEventApi([])).toStrictEqual([]) }) }) }) diff --git a/packages/plugin-electron-client-state-persistence/client-state-persistence.js b/packages/plugin-electron-client-state-persistence/client-state-persistence.js index 42186b4a59..49d557dc21 100644 --- a/packages/plugin-electron-client-state-persistence/client-state-persistence.js +++ b/packages/plugin-electron-client-state-persistence/client-state-persistence.js @@ -1,4 +1,4 @@ -const featureFlagDelegate = require('@bugsnag/core/lib/feature-flag-delegate') +const { featureFlagDelegate } = require('@bugsnag/core') const isEnabledFor = client => client._config.autoDetectErrors && client._config.enabledErrorTypes.nativeCrashes diff --git a/packages/plugin-electron-deliver-minidumps/deliver-minidumps.js b/packages/plugin-electron-deliver-minidumps/deliver-minidumps.js index d58d224408..e2f60c88eb 100644 --- a/packages/plugin-electron-deliver-minidumps/deliver-minidumps.js +++ b/packages/plugin-electron-deliver-minidumps/deliver-minidumps.js @@ -5,7 +5,7 @@ const MinidumpDeliveryLoop = require('./minidump-loop') const MinidumpQueue = require('./minidump-queue') const sendMinidumpFactory = require('./send-minidump') const NetworkStatus = require('@bugsnag/electron-network-status') -const featureFlagDelegate = require('@bugsnag/core/lib/feature-flag-delegate') +const { featureFlagDelegate } = require('@bugsnag/core') const isEnabledFor = client => client._config.autoDetectErrors && client._config.enabledErrorTypes.nativeCrashes diff --git a/packages/plugin-electron-ipc/bugsnag-ipc-main.js b/packages/plugin-electron-ipc/bugsnag-ipc-main.js index 3d61c8c0d5..5662492e3e 100644 --- a/packages/plugin-electron-ipc/bugsnag-ipc-main.js +++ b/packages/plugin-electron-ipc/bugsnag-ipc-main.js @@ -1,6 +1,5 @@ -const { Breadcrumb, Event } = require('@bugsnag/core') +const { Breadcrumb, Event, featureFlagDelegate } = require('@bugsnag/core') const runCallbacks = require('@bugsnag/core/lib/callback-runner') -const featureFlagDelegate = require('@bugsnag/core/lib/feature-flag-delegate') module.exports = class BugsnagIpcMain { constructor (client) { diff --git a/packages/plugin-electron-renderer-event-data/renderer-event-data.js b/packages/plugin-electron-renderer-event-data/renderer-event-data.js index 2ba001f5d7..e20b03dfd9 100644 --- a/packages/plugin-electron-renderer-event-data/renderer-event-data.js +++ b/packages/plugin-electron-renderer-event-data/renderer-event-data.js @@ -1,5 +1,5 @@ const { stripProjectRoot } = require('@bugsnag/plugin-electron-renderer-strip-project-root') -const featureFlagDelegate = require('@bugsnag/core/lib/feature-flag-delegate') +const { featureFlagDelegate } = require('@bugsnag/core') module.exports = (BugsnagIpcRenderer = window.__bugsnag_ipc__) => ({ load: client => { From c0bbe1714a8f4bc475eee7f1f1a17cbeadba802d Mon Sep 17 00:00:00 2001 From: AnastasiiaSvietlova Date: Mon, 9 Jun 2025 13:56:11 +0200 Subject: [PATCH 2/2] add some types --- packages/core/src/lib/feature-flag-delegate.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/src/lib/feature-flag-delegate.ts b/packages/core/src/lib/feature-flag-delegate.ts index e03a5a4a80..39d33b977e 100644 --- a/packages/core/src/lib/feature-flag-delegate.ts +++ b/packages/core/src/lib/feature-flag-delegate.ts @@ -74,6 +74,7 @@ const featureFlagDelegate: FeatureFlagDelegate = { return result; }); }, + clear: (features, featuresIndex, name) => { const existingIndex = featuresIndex[name] if (typeof existingIndex === 'number') {