Skip to content

Commit

Permalink
Merge pull request #1802 from bugsnag/PLAT-8735-preserve-feature-flag…
Browse files Browse the repository at this point in the history
…-order

[PLAT-8735] Preserve feature flag order
  • Loading branch information
gingerbenw authored Sep 2, 2022
2 parents a11d839 + aa5d206 commit 88a2e3d
Show file tree
Hide file tree
Showing 19 changed files with 262 additions and 202 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

- (react-native) Update bugsnag-cocoa from v6.20.0 to [v6.22.2](https://github.com/bugsnag/bugsnag-cocoa/blob/master/CHANGELOG.md#6222-2022-08-17)
- (react-native) Update bugsnag-android from v5.24.0 to [v5.26.0](https://github.com/bugsnag/bugsnag-android/blob/master/CHANGELOG.md#5260-2022-08-18)
- Refactor feature flags to maintain insertion order [#1802](https://github.com/bugsnag/bugsnag-js/pull/1802)

## v7.17.3 (2022-07-18)

Expand Down
6 changes: 3 additions & 3 deletions packages/core/client.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Client, OnErrorCallback, Config, Breadcrumb, Session, OnSessionCallback, OnBreadcrumbCallback, Plugin, Device, App, User } from './types'
import { Client, OnErrorCallback, Config, Breadcrumb, Session, OnSessionCallback, OnBreadcrumbCallback, Plugin, Device, App, User, FeatureFlag } from './types'
import EventWithInternals from './event'

interface LoggerConfig {
Expand Down Expand Up @@ -43,14 +43,14 @@ export default class ClientWithInternals<T extends Config = Config> extends Clie
_config: T
_depth: number
_logger: LoggerConfig
_breadcrumbs: Breadcrumb[];
_breadcrumbs: Breadcrumb[]
_delivery: Delivery
_setDelivery: (handler: (client: Client) => Delivery) => void

_user: User

_metadata: { [key: string]: any }
_features: { [key: string]: string | null }
_features: FeatureFlag | null[]

startSession(): ClientWithInternals
resumeSession(): ClientWithInternals
Expand Down
18 changes: 10 additions & 8 deletions packages/core/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ const reduce = require('./lib/es-utils/reduce')
const keys = require('./lib/es-utils/keys')
const assign = require('./lib/es-utils/assign')
const runCallbacks = require('./lib/callback-runner')
const featureFlagDelegate = require('./lib/feature-flag-delegate')
const metadataDelegate = require('./lib/metadata-delegate')
const runSyncCallbacks = require('./lib/sync-callback-runner')
const BREADCRUMB_TYPES = require('./lib/breadcrumb-types')
const { add, clear, merge } = require('./lib/feature-flag-delegate')

const noop = () => {}

Expand All @@ -36,7 +36,8 @@ class Client {
this._breadcrumbs = []
this._session = null
this._metadata = {}
this._features = {}
this._featuresIndex = {}
this._features = []
this._context = undefined
this._user = {}

Expand Down Expand Up @@ -90,19 +91,20 @@ class Client {
}

addFeatureFlag (name, variant = null) {
featureFlagDelegate.add(this._features, name, variant)
add(this._features, this._featuresIndex, name, variant)
}

addFeatureFlags (featureFlags) {
featureFlagDelegate.merge(this._features, featureFlags)
merge(this._features, featureFlags, this._featuresIndex)
}

clearFeatureFlag (name) {
delete this._features[name]
clear(this._features, this._featuresIndex, name)
}

clearFeatureFlags () {
this._features = {}
this._features = []
this._featuresIndex = {}
}

getContext () {
Expand Down Expand Up @@ -151,7 +153,7 @@ class Client {

// update and elevate some options
this._metadata = assign({}, config.metadata)
featureFlagDelegate.merge(this._features, config.featureFlags)
merge(this._features, config.featureFlags, this._featuresIndex)
this._user = assign({}, config.user)
this._context = config.context
if (config.logger) this._logger = config.logger
Expand Down Expand Up @@ -295,9 +297,9 @@ class Client {
})
event.context = event.context || this._context
event._metadata = assign({}, event._metadata, this._metadata)
event._features = assign({}, event._features, this._features)
event._user = assign({}, event._user, this._user)
event.breadcrumbs = this._breadcrumbs.slice()
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)) {
Expand Down
5 changes: 3 additions & 2 deletions packages/core/event.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { App, Device, Event, Request, Breadcrumb, User, Session } from './types'
import { App, Device, Event, Request, Breadcrumb, User, Session, FeatureFlag } from './types'
import { Error } from './types/event'

interface HandledState {
Expand All @@ -21,7 +21,8 @@ interface FeatureFlagPayload {
export default class EventWithInternals extends Event {
constructor (errorClass: string, errorMessage: string, stacktrace: any[], handledState?: HandledState, originalError?: Error)
_metadata: { [key: string]: any }
_features: { [key: string]: string | null }
_features: FeatureFlag | null[]
_featuresIndex: { [key: string]: number }
_user: User
_handledState: HandledState
_session?: Session
Expand Down
12 changes: 7 additions & 5 deletions packages/core/event.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ class Event {
this.threads = []

this._metadata = {}
this._features = {}
this._features = []
this._featuresIndex = {}
this._user = {}
this._session = undefined

Expand Down Expand Up @@ -56,19 +57,20 @@ class Event {
}

addFeatureFlag (name, variant = null) {
featureFlagDelegate.add(this._features, name, variant)
featureFlagDelegate.add(this._features, this._featuresIndex, name, variant)
}

addFeatureFlags (featureFlags) {
featureFlagDelegate.merge(this._features, featureFlags)
featureFlagDelegate.merge(this._features, featureFlags, this._featuresIndex)
}

clearFeatureFlag (name) {
delete this._features[name]
featureFlagDelegate.clear(this._features, this._featuresIndex, name)
}

clearFeatureFlags () {
this._features = {}
this._features = []
this._featuresIndex = {}
}

getUser () {
Expand Down
3 changes: 2 additions & 1 deletion packages/core/lib/clone-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ module.exports = (client) => {
// so ensure they are are (shallow) cloned
clone._breadcrumbs = client._breadcrumbs.slice()
clone._metadata = assign({}, client._metadata)
clone._features = assign({}, client._features)
clone._features = [...client._features]
clone._featuresIndex = assign({}, client._featuresIndex)
clone._user = assign({}, client._user)
clone._context = client._context

Expand Down
37 changes: 27 additions & 10 deletions packages/core/lib/feature-flag-delegate.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
const map = require('./es-utils/map')
const keys = require('./es-utils/keys')
const filter = require('./es-utils/filter')
const isArray = require('./es-utils/is-array')
const jsonStringify = require('@bugsnag/safe-json-stringify')

function add (existingFeatures, name, variant) {
function add (existingFeatures, existingFeatureKeys, name, variant) {
if (typeof name !== 'string') {
return
}
Expand All @@ -14,10 +14,17 @@ function add (existingFeatures, name, variant) {
variant = jsonStringify(variant)
}

existingFeatures[name] = 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) {
function merge (existingFeatures, newFeatures, existingFeatureKeys) {
if (!isArray(newFeatures)) {
return
}
Expand All @@ -30,27 +37,37 @@ function merge (existingFeatures, newFeatures) {
}

// 'add' will handle if 'name' doesn't exist & 'variant' is optional
add(existingFeatures, feature.name, feature.variant)
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 map(
keys(featureFlags),
name => {
filter(featureFlags, Boolean),
({ name, variant }) => {
const flag = { featureFlag: name }

// don't add a 'variant' property unless there's actually a value
if (typeof featureFlags[name] === 'string') {
flag.variant = featureFlags[name]
if (typeof variant === 'string') {
flag.variant = variant
}

return flag
}
)
}

module.exports = { add, merge, toEventApi }
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 }
6 changes: 3 additions & 3 deletions packages/core/lib/test/clone-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,14 +116,14 @@ describe('clone-client', () => {

const clone = cloneClient(original)

expect(clone._features).toStrictEqual({ abc: '123' })
expect(clone._features).toStrictEqual([{ name: 'abc', variant: '123' }])
expect(clone._features).not.toBe(original._features)

// changing the clone's feature flags shouldn't affect the original
clone.addFeatureFlag('xyz', '999')

expect(clone._features).toStrictEqual({ abc: '123', xyz: '999' })
expect(original._features).toStrictEqual({ abc: '123' })
expect(clone._features).toStrictEqual([{ name: 'abc', variant: '123' }, { name: 'xyz', variant: '999' }])
expect(original._features).toStrictEqual([{ name: 'abc', variant: '123' }])
})

it('should clone user information', () => {
Expand Down
Loading

0 comments on commit 88a2e3d

Please sign in to comment.