Skip to content

Commit c33803c

Browse files
[8.19] [Investigations][Timeline] - Run Object.keys less frequently (#219629) (#219749)
# Backport This will backport the following commits from `main` to `8.19`: - [[Investigations][Timeline] - Run Object.keys less frequently (#219629)](#219629) <!--- Backport version: 9.6.6 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sorenlouv/backport) <!--BACKPORT [{"author":{"name":"Michael Olorunnisola","email":"[email protected]"},"sourceCommit":{"committedDate":"2025-04-30T14:50:46Z","message":"[Investigations][Timeline] - Run Object.keys less frequently (#219629)\n\n## Summary\n\nThis PR makes a minor performance improvement by pulling out the\n`Object.keys` call on `hit.fields` out of any unnecessary loops.\n\n---------\n\nCo-authored-by: kibanamachine <[email protected]>","sha":"fd4ff0a2e872d7ba778c3de138e5919a29d61220","branchLabelMapping":{"^v9.1.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","Team:Threat Hunting:Investigations","backport:version","v9.1.0","v8.19.0","v9.0.1","v8.18.2"],"title":"[Investigations][Timeline] - Run Object.keys less frequently","number":219629,"url":"https://github.com/elastic/kibana/pull/219629","mergeCommit":{"message":"[Investigations][Timeline] - Run Object.keys less frequently (#219629)\n\n## Summary\n\nThis PR makes a minor performance improvement by pulling out the\n`Object.keys` call on `hit.fields` out of any unnecessary loops.\n\n---------\n\nCo-authored-by: kibanamachine <[email protected]>","sha":"fd4ff0a2e872d7ba778c3de138e5919a29d61220"}},"sourceBranch":"main","suggestedTargetBranches":["8.19","9.0","8.18"],"targetPullRequestStates":[{"branch":"main","label":"v9.1.0","branchLabelMappingKey":"^v9.1.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/219629","number":219629,"mergeCommit":{"message":"[Investigations][Timeline] - Run Object.keys less frequently (#219629)\n\n## Summary\n\nThis PR makes a minor performance improvement by pulling out the\n`Object.keys` call on `hit.fields` out of any unnecessary loops.\n\n---------\n\nCo-authored-by: kibanamachine <[email protected]>","sha":"fd4ff0a2e872d7ba778c3de138e5919a29d61220"}},{"branch":"8.19","label":"v8.19.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"9.0","label":"v9.0.1","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"8.18","label":"v8.18.2","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"}]}] BACKPORT--> Co-authored-by: Michael Olorunnisola <[email protected]>
1 parent 1ef4833 commit c33803c

File tree

6 files changed

+42
-27
lines changed

6 files changed

+42
-27
lines changed

x-pack/platform/plugins/shared/timelines/server/search_strategy/timeline/factory/helpers/build_ecs_objects.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,16 @@ import { getNestedParentPath } from './get_nested_parent_path';
1515

1616
export const buildEcsObjects = (hit: EventHit): Ecs => {
1717
const ecsFields = [...TIMELINE_EVENTS_FIELDS];
18+
const fieldsKeys = Object.keys(hit.fields ?? {});
1819
return ecsFields.reduce(
1920
(acc, field) => {
20-
const nestedParentPath = getNestedParentPath(field, hit.fields);
21+
const nestedParentPath = getNestedParentPath(field, fieldsKeys);
2122
if (
2223
nestedParentPath != null ||
2324
has(field, hit.fields) ||
2425
ECS_METADATA_FIELDS.includes(field)
2526
) {
26-
return merge(acc, buildObjectRecursive(field, hit.fields));
27+
return merge(acc, buildObjectRecursive(field, hit.fields, fieldsKeys));
2728
}
2829
return acc;
2930
},

x-pack/platform/plugins/shared/timelines/server/search_strategy/timeline/factory/helpers/build_object_recursive.test.ts

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,17 @@ import { EventHit } from '../../../../../common/search_strategy';
1010
import { buildObjectRecursive } from './build_object_recursive';
1111

1212
describe('buildObjectRecursive', () => {
13+
const eventHitKeys = Object.keys(eventHit.fields ?? {});
1314
it('builds an object from a single non-nested field', () => {
14-
expect(buildObjectRecursive('@timestamp', eventHit.fields)).toEqual({
15+
expect(buildObjectRecursive('@timestamp', eventHit.fields, eventHitKeys)).toEqual({
1516
'@timestamp': ['2020-11-17T14:48:08.922Z'],
1617
});
1718
});
1819

1920
it('builds an object with no fields response', () => {
2021
const { fields, ...fieldLessHit } = eventHit;
2122
// @ts-expect-error fieldLessHit is intentionally missing fields
22-
expect(buildObjectRecursive('@timestamp', fieldLessHit)).toEqual({
23+
expect(buildObjectRecursive('@timestamp', fieldLessHit, eventHitKeys)).toEqual({
2324
'@timestamp': [],
2425
});
2526
});
@@ -33,7 +34,7 @@ describe('buildObjectRecursive', () => {
3334
},
3435
};
3536

36-
expect(buildObjectRecursive('foo.barBaz', hit.fields)).toEqual({
37+
expect(buildObjectRecursive('foo.barBaz', hit.fields, eventHitKeys)).toEqual({
3738
foo: { barBaz: ['foo'] },
3839
});
3940
});
@@ -45,7 +46,8 @@ describe('buildObjectRecursive', () => {
4546
foo: [{ bar: ['baz'] }],
4647
},
4748
};
48-
expect(buildObjectRecursive('foo.bar', hit.fields)).toEqual({
49+
const hitKeys = Object.keys(hit.fields ?? {});
50+
expect(buildObjectRecursive('foo.bar', hit.fields, hitKeys)).toEqual({
4951
foo: [{ bar: ['baz'] }],
5052
});
5153
});
@@ -61,7 +63,8 @@ describe('buildObjectRecursive', () => {
6163
],
6264
},
6365
};
64-
expect(buildObjectRecursive('foo.bar.baz', nestedHit.fields)).toEqual({
66+
const nestedHitKeys = Object.keys(nestedHit.fields ?? {});
67+
expect(buildObjectRecursive('foo.bar.baz', nestedHit.fields, nestedHitKeys)).toEqual({
6568
foo: {
6669
bar: [
6770
{
@@ -73,7 +76,9 @@ describe('buildObjectRecursive', () => {
7376
});
7477

7578
it('builds intermediate objects at multiple levels', () => {
76-
expect(buildObjectRecursive('threat.enrichments.matched.atomic', eventHit.fields)).toEqual({
79+
expect(
80+
buildObjectRecursive('threat.enrichments.matched.atomic', eventHit.fields, eventHitKeys)
81+
).toEqual({
7782
threat: {
7883
enrichments: [
7984
{
@@ -117,7 +122,9 @@ describe('buildObjectRecursive', () => {
117122
});
118123

119124
it('preserves multiple values for a single leaf', () => {
120-
expect(buildObjectRecursive('threat.enrichments.matched.field', eventHit.fields)).toEqual({
125+
expect(
126+
buildObjectRecursive('threat.enrichments.matched.field', eventHit.fields, eventHitKeys)
127+
).toEqual({
121128
threat: {
122129
enrichments: [
123130
{
@@ -162,6 +169,7 @@ describe('buildObjectRecursive', () => {
162169

163170
describe('multiple levels of nested fields', () => {
164171
let nestedHit: EventHit;
172+
let nestedHitKeys: string[];
165173

166174
beforeEach(() => {
167175
// @ts-expect-error nestedHit is minimal
@@ -183,10 +191,13 @@ describe('buildObjectRecursive', () => {
183191
],
184192
},
185193
};
194+
nestedHitKeys = Object.keys(nestedHit.fields ?? {});
186195
});
187196

188197
it('includes objects without the field', () => {
189-
expect(buildObjectRecursive('nested_1.foo.nested_2.bar.leaf', nestedHit.fields)).toEqual({
198+
expect(
199+
buildObjectRecursive('nested_1.foo.nested_2.bar.leaf', nestedHit.fields, nestedHitKeys)
200+
).toEqual({
190201
nested_1: {
191202
foo: [
192203
{
@@ -205,7 +216,9 @@ describe('buildObjectRecursive', () => {
205216
});
206217

207218
it('groups multiple leaf values', () => {
208-
expect(buildObjectRecursive('nested_1.foo.nested_2.bar.leaf_2', nestedHit.fields)).toEqual({
219+
expect(
220+
buildObjectRecursive('nested_1.foo.nested_2.bar.leaf_2', nestedHit.fields, nestedHitKeys)
221+
).toEqual({
209222
nested_1: {
210223
foo: [
211224
{

x-pack/platform/plugins/shared/timelines/server/search_strategy/timeline/factory/helpers/build_object_recursive.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,12 @@ import { Fields } from '../../../../../common/search_strategy';
1212
import { toStringArray } from '../../../../../common/utils/to_array';
1313
import { getNestedParentPath } from './get_nested_parent_path';
1414

15-
export const buildObjectRecursive = (fieldPath: string, fields: Fields): Partial<Ecs> => {
16-
const nestedParentPath = getNestedParentPath(fieldPath, fields);
15+
export const buildObjectRecursive = (
16+
fieldPath: string,
17+
fields: Fields,
18+
fieldsKeys: string[]
19+
): Partial<Ecs> => {
20+
const nestedParentPath = getNestedParentPath(fieldPath, fieldsKeys);
1721
if (!nestedParentPath) {
1822
return set({}, fieldPath, toStringArray(get(fieldPath, fields)));
1923
}
@@ -23,6 +27,6 @@ export const buildObjectRecursive = (fieldPath: string, fields: Fields): Partial
2327
return set(
2428
{},
2529
nestedParentPath,
26-
subFields.map((subField) => buildObjectRecursive(subPath, subField))
30+
subFields.map((subField) => buildObjectRecursive(subPath, subField, Object.keys(subField)))
2731
);
2832
};

x-pack/platform/plugins/shared/timelines/server/search_strategy/timeline/factory/helpers/format_timeline_data.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,9 +109,10 @@ export const formatTimelineData = async (
109109
}
110110

111111
result.node.data = [];
112+
const hitFieldKeys = Object.keys(hit.fields || {});
112113

113114
for (const fieldName of uniqueFields) {
114-
const nestedParentPath = getNestedParentPath(fieldName, hit.fields);
115+
const nestedParentPath = getNestedParentPath(fieldName, hitFieldKeys);
115116
const isEcs = ECS_METADATA_FIELDS.includes(fieldName);
116117
if (!nestedParentPath && !has(fieldName, hit.fields) && !isEcs) {
117118
// eslint-disable-next-line no-continue
@@ -127,7 +128,7 @@ export const formatTimelineData = async (
127128
}
128129

129130
if (ecsFieldSet.has(fieldName)) {
130-
deepMerge(result.node.ecs, buildObjectRecursive(fieldName, hit.fields));
131+
deepMerge(result.node.ecs, buildObjectRecursive(fieldName, hit.fields, hitFieldKeys));
131132
}
132133
}
133134

x-pack/platform/plugins/shared/timelines/server/search_strategy/timeline/factory/helpers/get_nested_parent_path.test.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { getNestedParentPath } from './get_nested_parent_path';
99

1010
describe('getNestedParentPath', () => {
1111
let testFields: Fields | undefined;
12+
let testFieldsKeys: string[];
1213
beforeAll(() => {
1314
testFields = {
1415
'not.nested': ['I am not nested'],
@@ -18,22 +19,23 @@ describe('getNestedParentPath', () => {
1819
},
1920
],
2021
};
22+
testFieldsKeys = Object.keys(testFields);
2123
});
2224

2325
it('should ignore fields that are not nested', () => {
2426
const notNestedPath = 'not.nested';
25-
const shouldBeUndefined = getNestedParentPath(notNestedPath, testFields);
27+
const shouldBeUndefined = getNestedParentPath(notNestedPath, testFieldsKeys);
2628
expect(shouldBeUndefined).toBe(undefined);
2729
});
2830

2931
it('should capture fields that are nested', () => {
3032
const nestedPath = 'is.nested.field';
31-
const nestedParentPath = getNestedParentPath(nestedPath, testFields);
33+
const nestedParentPath = getNestedParentPath(nestedPath, testFieldsKeys);
3234
expect(nestedParentPath).toEqual('is.nested');
3335
});
3436

3537
it('should return undefined when the `fields` param is undefined', () => {
3638
const nestedPath = 'is.nested.field';
37-
expect(getNestedParentPath(nestedPath, undefined)).toBe(undefined);
39+
expect(getNestedParentPath(nestedPath, [])).toBe(undefined);
3840
});
3941
});

x-pack/platform/plugins/shared/timelines/server/search_strategy/timeline/factory/helpers/get_nested_parent_path.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,8 @@
55
* 2.0.
66
*/
77

8-
import { Fields } from '../../../../../common/search_strategy';
9-
108
/**
119
* If a prefix of our full field path is present as a field, we know that our field is nested
1210
*/
13-
export const getNestedParentPath = (
14-
fieldPath: string,
15-
fields: Fields | undefined
16-
): string | undefined =>
17-
fields &&
18-
Object.keys(fields).find((field) => field !== fieldPath && fieldPath.startsWith(`${field}.`));
11+
export const getNestedParentPath = (fieldPath: string, fields: string[]): string | undefined =>
12+
fields.find((field) => field !== fieldPath && fieldPath.startsWith(`${field}.`));

0 commit comments

Comments
 (0)