Skip to content
Draft
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
39 changes: 39 additions & 0 deletions packages/cli-kit/src/public/common/object.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
deepCompare,
deepDifference,
deepMergeObjects,
deepStripUndefined,
getPathValue,
mapValues,
pickBy,
Expand Down Expand Up @@ -440,3 +441,41 @@ describe('unsetPathValue', () => {
expect(obj).toEqual({regular: 'value2'})
})
})

describe('deepStripUndefined', () => {
test('removes top-level undefined fields', () => {
const obj = {a: 1, b: undefined, c: 'hello'}
expect(deepStripUndefined(obj)).toEqual({a: 1, c: 'hello'})
})

test('removes nested undefined fields', () => {
const obj = {outer: {a: 1, b: undefined}, c: 'hello'}
expect(deepStripUndefined(obj)).toEqual({outer: {a: 1}, c: 'hello'})
})

test('preserves arrays and recurses into them', () => {
const obj = {list: [{a: 1, b: undefined}, {c: undefined, d: 2}]}
expect(deepStripUndefined(obj)).toEqual({list: [{a: 1}, {d: 2}]})
})

test('returns primitives as-is', () => {
expect(deepStripUndefined('hello')).toBe('hello')
expect(deepStripUndefined(42)).toBe(42)
expect(deepStripUndefined(null)).toBe(null)
expect(deepStripUndefined(true)).toBe(true)
})

test('preserves null values (only strips undefined)', () => {
const obj = {a: null, b: undefined, c: 0, d: false, e: ''}
expect(deepStripUndefined(obj)).toEqual({a: null, c: 0, d: false, e: ''})
})

test('handles empty objects', () => {
expect(deepStripUndefined({})).toEqual({})
})

test('handles objects where all values are undefined', () => {
const obj = {a: undefined, b: undefined}
expect(deepStripUndefined(obj)).toEqual({})
})
})
23 changes: 23 additions & 0 deletions packages/cli-kit/src/public/common/object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,3 +134,26 @@ export function isEmpty(object: object): boolean {
export function compact(object: object): object {
return Object.fromEntries(Object.entries(object).filter(([_, value]) => value != null))
}

/**
* Recursively removes properties with `undefined` values from an object.
* Arrays are traversed but not filtered. Non-object values are returned as-is.
*
* @param value - The value to strip undefined fields from.
* @returns A deep copy of the value with all undefined-valued keys removed.
*/
export function deepStripUndefined<T>(value: T): T {
if (Array.isArray(value)) {
return value.map(deepStripUndefined) as T
}
if (value !== null && typeof value === 'object') {
const result: Record<string, unknown> = {}
for (const [key, val] of Object.entries(value)) {
if (val !== undefined) {
result[key] = deepStripUndefined(val)
}
}
return result as T
}
return value
}
3 changes: 2 additions & 1 deletion packages/cli-kit/src/public/node/analytics.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {alwaysLogAnalytics, alwaysLogMetrics, analyticsDisabled, isShopify} from './context/local.js'
import {deepStripUndefined} from '../common/object.js'
import * as metadata from './metadata.js'
import {publishMonorailEvent, MONORAIL_COMMAND_TOPIC} from './monorail.js'
import {fanoutHooks} from './plugins.js'
Expand Down Expand Up @@ -193,7 +194,7 @@ async function buildPayload({config, errorMessage, exitMode}: ReportAnalyticsEve
})

// strip undefined fields -- they make up the majority of payloads due to wide metadata structure.
payload = JSON.parse(JSON.stringify(payload))
payload = deepStripUndefined(payload)

return sanitizePayload(payload)
}
Expand Down
Loading