Skip to content

Commit 638778b

Browse files
authored
feat(core): add value length cap to partialClone (aws#7150)
## Problem If we want to log a large json object with giant strings (think whole files), it can make the logs difficult to read. This is especially relevant when the underlying string values are not very important. ## Solution - allow the string values to be truncated with `maxStringLength` option. ## Notes I am planning to port this utility to Flare to improve the logging experience there, and want this functionality to exist there however I thought this could be useful here too. --- - Treat all work as PUBLIC. Private `feature/x` branches will not be squash-merged at release time. - Your code changes must meet the guidelines in [CONTRIBUTING.md](https://github.com/aws/aws-toolkit-vscode/blob/master/CONTRIBUTING.md#guidelines). - License: I confirm that my contribution is made under the terms of the Apache 2.0 license.
1 parent 8ece808 commit 638778b

File tree

6 files changed

+80
-42
lines changed

6 files changed

+80
-42
lines changed

packages/amazonq/test/e2e/inline/inline.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ describe('Amazon Q Inline', async function () {
122122
.query({
123123
metricName: 'codewhisperer_userTriggerDecision',
124124
})
125-
.map((e) => collectionUtil.partialClone(e, 3, ['credentialStartUrl'], '[omitted]'))
125+
.map((e) => collectionUtil.partialClone(e, 3, ['credentialStartUrl'], { replacement: '[omitted]' }))
126126
}
127127

128128
for (const [name, invokeCompletion] of [

packages/core/src/auth/sso/clients.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ function addLoggingMiddleware(client: SSOOIDCClient) {
258258
args.input as unknown as Record<string, unknown>,
259259
3,
260260
['clientSecret', 'accessToken', 'refreshToken'],
261-
'[omitted]'
261+
{ replacement: '[omitted]' }
262262
)
263263
getLogger().debug('API request (%s %s): %O', hostname, path, input)
264264
}
@@ -288,7 +288,7 @@ function addLoggingMiddleware(client: SSOOIDCClient) {
288288
result.output as unknown as Record<string, unknown>,
289289
3,
290290
['clientSecret', 'accessToken', 'refreshToken'],
291-
'[omitted]'
291+
{ replacement: '[omitted]' }
292292
)
293293
getLogger().debug('API response (%s %s): %O', hostname, path, output)
294294
}

packages/core/src/shared/utilities/collectionUtils.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { isWeb } from '../extensionGlobals'
77
import { inspect as nodeInspect } from 'util'
88
import { AsyncCollection, toCollection } from './asyncCollection'
99
import { SharedProp, AccumulableKeys, Coalesce, isNonNullable } from './tsUtils'
10+
import { truncate } from './textUtilities'
1011

1112
export function union<T>(a: Iterable<T>, b: Iterable<T>): Set<T> {
1213
const result = new Set<T>()
@@ -304,26 +305,38 @@ export function assign<T extends Record<any, any>, U extends Partial<T>>(data: T
304305
* @param depth
305306
* @param omitKeys Omit properties matching these names (at any depth).
306307
* @param replacement Replacement for object whose fields extend beyond `depth`, and properties matching `omitKeys`.
308+
* @param maxStringLength truncates string values that exceed this threshold (includes values in nested arrays)
307309
*/
308-
export function partialClone(obj: any, depth: number = 3, omitKeys: string[] = [], replacement?: any): any {
310+
export function partialClone(
311+
obj: any,
312+
depth: number = 3,
313+
omitKeys: string[] = [],
314+
options?: {
315+
replacement?: any
316+
maxStringLength?: number
317+
}
318+
): any {
309319
// Base case: If input is not an object or has no children, return it.
310320
if (typeof obj !== 'object' || obj === null || 0 === Object.getOwnPropertyNames(obj).length) {
321+
if (typeof obj === 'string' && options?.maxStringLength) {
322+
return truncate(obj, options?.maxStringLength, '...')
323+
}
311324
return obj
312325
}
313326

314327
// Create a new object of the same type as the input object.
315328
const clonedObj = Array.isArray(obj) ? [] : {}
316329

317330
if (depth === 0) {
318-
return replacement ? replacement : clonedObj
331+
return options?.replacement ? options.replacement : clonedObj
319332
}
320333

321334
// Recursively clone properties of the input object
322335
for (const key in obj) {
323336
if (omitKeys.includes(key)) {
324-
;(clonedObj as any)[key] = replacement ? replacement : Array.isArray(obj) ? [] : {}
337+
;(clonedObj as any)[key] = options?.replacement ? options.replacement : Array.isArray(obj) ? [] : {}
325338
} else if (Object.prototype.hasOwnProperty.call(obj, key)) {
326-
;(clonedObj as any)[key] = partialClone(obj[key], depth - 1, omitKeys, replacement)
339+
;(clonedObj as any)[key] = partialClone(obj[key], depth - 1, omitKeys, options)
327340
}
328341
}
329342

packages/core/src/shared/utilities/textUtilities.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { default as stripAnsi } from 'strip-ansi'
1010
import { getLogger } from '../logger/logger'
1111

1212
/**
13-
* Truncates string `s` if it exceeds `n` chars.
13+
* Truncates string `s` if it has or exceeds `n` chars.
1414
*
1515
* If `n` is negative, truncates at start instead of end.
1616
*

packages/core/src/shared/vscode/commands2.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -653,7 +653,7 @@ async function runCommand<T extends Callback>(fn: T, info: CommandInfo<T>): Prom
653653

654654
logger.debug(
655655
`command: running ${label} with arguments: %O`,
656-
partialClone(args, 3, ['clientSecret', 'accessToken', 'refreshToken', 'tooltip'], '[omitted]')
656+
partialClone(args, 3, ['clientSecret', 'accessToken', 'refreshToken', 'tooltip'], { replacement: '[omitted]' })
657657
)
658658

659659
try {

packages/core/src/test/shared/utilities/collectionUtils.test.ts

Lines changed: 58 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -710,8 +710,10 @@ describe('CollectionUtils', async function () {
710710
})
711711

712712
describe('partialClone', function () {
713-
it('omits properties by depth', function () {
714-
const testObj = {
713+
let multipleTypedObj: object
714+
715+
before(async function () {
716+
multipleTypedObj = {
715717
a: 34234234234,
716718
b: '123456789',
717719
c: new Date(2023, 1, 1),
@@ -724,57 +726,80 @@ describe('CollectionUtils', async function () {
724726
throw Error()
725727
},
726728
}
729+
})
727730

728-
assert.deepStrictEqual(partialClone(testObj, 1), {
729-
...testObj,
731+
it('omits properties by depth', function () {
732+
assert.deepStrictEqual(partialClone(multipleTypedObj, 1), {
733+
...multipleTypedObj,
730734
d: {},
731735
e: {},
732736
})
733-
assert.deepStrictEqual(partialClone(testObj, 0, [], '[replaced]'), '[replaced]')
734-
assert.deepStrictEqual(partialClone(testObj, 1, [], '[replaced]'), {
735-
...testObj,
737+
assert.deepStrictEqual(partialClone(multipleTypedObj, 0, [], { replacement: '[replaced]' }), '[replaced]')
738+
assert.deepStrictEqual(partialClone(multipleTypedObj, 1, [], { replacement: '[replaced]' }), {
739+
...multipleTypedObj,
736740
d: '[replaced]',
737741
e: '[replaced]',
738742
})
739-
assert.deepStrictEqual(partialClone(testObj, 3), {
740-
...testObj,
743+
assert.deepStrictEqual(partialClone(multipleTypedObj, 3), {
744+
...multipleTypedObj,
741745
d: { d1: { d2: {} } },
742746
})
743-
assert.deepStrictEqual(partialClone(testObj, 4), testObj)
747+
assert.deepStrictEqual(partialClone(multipleTypedObj, 4), multipleTypedObj)
744748
})
745749

746750
it('omits properties by name', function () {
747-
const testObj = {
748-
a: 34234234234,
749-
b: '123456789',
750-
c: new Date(2023, 1, 1),
751-
d: { d1: { d2: { d3: 'deep' } } },
751+
assert.deepStrictEqual(partialClone(multipleTypedObj, 2, ['c', 'e2'], { replacement: '[replaced]' }), {
752+
...multipleTypedObj,
753+
c: '[replaced]',
754+
d: { d1: '[replaced]' },
755+
e: {
756+
e1: '[replaced]',
757+
e2: '[replaced]',
758+
},
759+
})
760+
assert.deepStrictEqual(partialClone(multipleTypedObj, 3, ['c', 'e2'], { replacement: '[replaced]' }), {
761+
...multipleTypedObj,
762+
c: '[replaced]',
763+
d: { d1: { d2: '[replaced]' } },
752764
e: {
753765
e1: [4, 3, 7],
754-
e2: 'loooooooooo \n nnnnnnnnnnn \n gggggggg \n string',
766+
e2: '[replaced]',
755767
},
756-
f: () => {
757-
throw Error()
768+
})
769+
})
770+
771+
it('truncates properties by maxLength', function () {
772+
const testObj = {
773+
strValue: '1',
774+
boolValue: true,
775+
longString: '11111',
776+
nestedObj: {
777+
nestedObjAgain: {
778+
longNestedStr: '11111',
779+
shortNestedStr: '11',
780+
},
781+
},
782+
nestedObj2: {
783+
functionValue: (_: unknown) => {},
758784
},
785+
nestedObj3: {
786+
myArray: ['1', '11111', '1'],
787+
},
788+
objInArray: [{ shortString: '11', longString: '11111' }],
759789
}
760-
761-
assert.deepStrictEqual(partialClone(testObj, 2, ['c', 'e2'], '[omitted]'), {
790+
assert.deepStrictEqual(partialClone(testObj, 5, [], { maxStringLength: 2 }), {
762791
...testObj,
763-
c: '[omitted]',
764-
d: { d1: '[omitted]' },
765-
e: {
766-
e1: '[omitted]',
767-
e2: '[omitted]',
792+
longString: '11...',
793+
nestedObj: {
794+
nestedObjAgain: {
795+
longNestedStr: '11...',
796+
shortNestedStr: '11',
797+
},
768798
},
769-
})
770-
assert.deepStrictEqual(partialClone(testObj, 3, ['c', 'e2'], '[omitted]'), {
771-
...testObj,
772-
c: '[omitted]',
773-
d: { d1: { d2: '[omitted]' } },
774-
e: {
775-
e1: [4, 3, 7],
776-
e2: '[omitted]',
799+
nestedObj3: {
800+
myArray: ['1', '11...', '1'],
777801
},
802+
objInArray: [{ shortString: '11', longString: '11...' }],
778803
})
779804
})
780805
})

0 commit comments

Comments
 (0)