Skip to content

Commit 28e490e

Browse files
[9.1] [Discover][APM] Create accessKnownApmEventFields for validating unified traces API (#241976) (#242448)
# Backport This will backport the following commits from `main` to `9.1`: - [[Discover][APM] Create `accessKnownApmEventFields` for validating unified traces API (#241976)](#241976) <!--- Backport version: 10.1.0 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sorenlouv/backport) <!--BACKPORT [{"author":{"name":"Gonçalo Rica Pais da Silva","email":"[email protected]"},"sourceCommit":{"committedDate":"2025-11-10T15:47:33Z","message":"[Discover][APM] Create `accessKnownApmEventFields` for validating unified traces API (#241976)\n\n## Summary\n\nCloses #240857\n\nThis PR implements a new validation scheme for validating APM events,\nand applying it to the Unified Trace Waterfall APIs as an initial\nproving ground. `accessKnownApmEventFields` replaces\n`unflattenKnownApmEventFields` in its use for validating the\n`getUnifiedTraceItems` responses, removing a source of overhead with a\nmuch, much simpler and faster approach to validating and strongly typing\nthe APM Event. This changes what is at least an `O(n^2)` operation for\nan entire object, to just an `O(n)` operation per validation, with `n`\nbeing the number of required fields (which in reality is a small\nnumber).\n\nPotentially, if the checks are made lazy (when the field is accessed),\nthis could become `O(1)`, but with the downside that we won't be able to\ncollect all the missing fields, only the one which the access failed\nfor. With `accessKnownApmEventFields`, we are avoiding having to process\nan entire object just to have the correct single/multi values per field,\nwe only do it for the fields we are accessing and without needing to\nallocate any new objects/arrays whilst doing so. And the best part is we\nstill benefit from strong type checking and type inference around keys,\nso you still get auto-complete in the IDE.\n\nThe implementation proxies the original object, and then when we access\nthe fields, it resolves the single or multi-value types, as well as\nvalidating any required fields during construction. As part of the\nproxied object, we expose an `unflatten` method which then takes the\nvalidated object and well, unflattens it while still being type-checked.\nAs such, usage is as follows:\n\n```ts\nconst event = accessKnownApmEventFields(hit, [SERVICE_NAME]);\nconsole.log(event[SERVICE_NAME]); // outputs `\"node-svc\"` instead of `[\"node-svc\"]` as in the original object\nconst unflattened = event.unflatten();\nconsole.log(unflattened.service.name); // outputs `\"node-svc\"` like above\n```\n\nAs well in this PR is a couple of tweaks to\n`unflattenKnownApmEventFields` to improve perf there as well, by using a\n`Set` instead of array for `ALL_FIELDS` (making checks there go from\n`O(n)` to `O(1)` ), as well as removing `lodash` usage for Array\nchecking when there is standard JS APIs for this.\n\n## How to test\n\nThis PR needs to be able to pass CI, but also should not regress on the\nUI:\n\n- Go to Discover while in an Observability space, select a traces index\nin either classic or ES|QL mode.\n- Open a trace overview, the Focused Trace Waterfall should not error\nwhen trying to render the focused trace.\n- Open a full trace waterfall, that too should not error when trying to\nrender.\n- In both cases, traces should look as expected.","sha":"050b80dd7ceee80b4d5315e48001546e09e9ccc9","branchLabelMapping":{"^v9.3.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","Team:obs-ux-infra_services","backport:version","v9.3.0","v8.19.7","v9.1.7","v9.2.1"],"title":"[Discover][APM] Create `accessKnownApmEventFields` for validating unified traces API","number":241976,"url":"https://github.com/elastic/kibana/pull/241976","mergeCommit":{"message":"[Discover][APM] Create `accessKnownApmEventFields` for validating unified traces API (#241976)\n\n## Summary\n\nCloses #240857\n\nThis PR implements a new validation scheme for validating APM events,\nand applying it to the Unified Trace Waterfall APIs as an initial\nproving ground. `accessKnownApmEventFields` replaces\n`unflattenKnownApmEventFields` in its use for validating the\n`getUnifiedTraceItems` responses, removing a source of overhead with a\nmuch, much simpler and faster approach to validating and strongly typing\nthe APM Event. This changes what is at least an `O(n^2)` operation for\nan entire object, to just an `O(n)` operation per validation, with `n`\nbeing the number of required fields (which in reality is a small\nnumber).\n\nPotentially, if the checks are made lazy (when the field is accessed),\nthis could become `O(1)`, but with the downside that we won't be able to\ncollect all the missing fields, only the one which the access failed\nfor. With `accessKnownApmEventFields`, we are avoiding having to process\nan entire object just to have the correct single/multi values per field,\nwe only do it for the fields we are accessing and without needing to\nallocate any new objects/arrays whilst doing so. And the best part is we\nstill benefit from strong type checking and type inference around keys,\nso you still get auto-complete in the IDE.\n\nThe implementation proxies the original object, and then when we access\nthe fields, it resolves the single or multi-value types, as well as\nvalidating any required fields during construction. As part of the\nproxied object, we expose an `unflatten` method which then takes the\nvalidated object and well, unflattens it while still being type-checked.\nAs such, usage is as follows:\n\n```ts\nconst event = accessKnownApmEventFields(hit, [SERVICE_NAME]);\nconsole.log(event[SERVICE_NAME]); // outputs `\"node-svc\"` instead of `[\"node-svc\"]` as in the original object\nconst unflattened = event.unflatten();\nconsole.log(unflattened.service.name); // outputs `\"node-svc\"` like above\n```\n\nAs well in this PR is a couple of tweaks to\n`unflattenKnownApmEventFields` to improve perf there as well, by using a\n`Set` instead of array for `ALL_FIELDS` (making checks there go from\n`O(n)` to `O(1)` ), as well as removing `lodash` usage for Array\nchecking when there is standard JS APIs for this.\n\n## How to test\n\nThis PR needs to be able to pass CI, but also should not regress on the\nUI:\n\n- Go to Discover while in an Observability space, select a traces index\nin either classic or ES|QL mode.\n- Open a trace overview, the Focused Trace Waterfall should not error\nwhen trying to render the focused trace.\n- Open a full trace waterfall, that too should not error when trying to\nrender.\n- In both cases, traces should look as expected.","sha":"050b80dd7ceee80b4d5315e48001546e09e9ccc9"}},"sourceBranch":"main","suggestedTargetBranches":["8.19","9.1"],"targetPullRequestStates":[{"branch":"main","label":"v9.3.0","branchLabelMappingKey":"^v9.3.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/241976","number":241976,"mergeCommit":{"message":"[Discover][APM] Create `accessKnownApmEventFields` for validating unified traces API (#241976)\n\n## Summary\n\nCloses #240857\n\nThis PR implements a new validation scheme for validating APM events,\nand applying it to the Unified Trace Waterfall APIs as an initial\nproving ground. `accessKnownApmEventFields` replaces\n`unflattenKnownApmEventFields` in its use for validating the\n`getUnifiedTraceItems` responses, removing a source of overhead with a\nmuch, much simpler and faster approach to validating and strongly typing\nthe APM Event. This changes what is at least an `O(n^2)` operation for\nan entire object, to just an `O(n)` operation per validation, with `n`\nbeing the number of required fields (which in reality is a small\nnumber).\n\nPotentially, if the checks are made lazy (when the field is accessed),\nthis could become `O(1)`, but with the downside that we won't be able to\ncollect all the missing fields, only the one which the access failed\nfor. With `accessKnownApmEventFields`, we are avoiding having to process\nan entire object just to have the correct single/multi values per field,\nwe only do it for the fields we are accessing and without needing to\nallocate any new objects/arrays whilst doing so. And the best part is we\nstill benefit from strong type checking and type inference around keys,\nso you still get auto-complete in the IDE.\n\nThe implementation proxies the original object, and then when we access\nthe fields, it resolves the single or multi-value types, as well as\nvalidating any required fields during construction. As part of the\nproxied object, we expose an `unflatten` method which then takes the\nvalidated object and well, unflattens it while still being type-checked.\nAs such, usage is as follows:\n\n```ts\nconst event = accessKnownApmEventFields(hit, [SERVICE_NAME]);\nconsole.log(event[SERVICE_NAME]); // outputs `\"node-svc\"` instead of `[\"node-svc\"]` as in the original object\nconst unflattened = event.unflatten();\nconsole.log(unflattened.service.name); // outputs `\"node-svc\"` like above\n```\n\nAs well in this PR is a couple of tweaks to\n`unflattenKnownApmEventFields` to improve perf there as well, by using a\n`Set` instead of array for `ALL_FIELDS` (making checks there go from\n`O(n)` to `O(1)` ), as well as removing `lodash` usage for Array\nchecking when there is standard JS APIs for this.\n\n## How to test\n\nThis PR needs to be able to pass CI, but also should not regress on the\nUI:\n\n- Go to Discover while in an Observability space, select a traces index\nin either classic or ES|QL mode.\n- Open a trace overview, the Focused Trace Waterfall should not error\nwhen trying to render the focused trace.\n- Open a full trace waterfall, that too should not error when trying to\nrender.\n- In both cases, traces should look as expected.","sha":"050b80dd7ceee80b4d5315e48001546e09e9ccc9"}},{"branch":"8.19","label":"v8.19.7","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"9.1","label":"v9.1.7","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"9.2","label":"v9.2.1","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"url":"https://github.com/elastic/kibana/pull/242443","number":242443,"state":"OPEN"}]}] BACKPORT--> --------- Co-authored-by: kibanamachine <[email protected]>
1 parent 80cb77f commit 28e490e

File tree

10 files changed

+326
-110
lines changed

10 files changed

+326
-110
lines changed

x-pack/solutions/observability/plugins/apm/server/routes/services/get_service_instance_metadata_details.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { merge } from 'lodash';
88
import { rangeQuery } from '@kbn/observability-plugin/server';
99
import { ProcessorEvent } from '@kbn/observability-plugin/common';
1010
import { unflattenKnownApmEventFields } from '@kbn/apm-data-access-plugin/server/utils';
11-
import type { FlattenedApmEvent } from '@kbn/apm-data-access-plugin/server/utils/unflatten_known_fields';
11+
import type { FlattenedApmEvent } from '@kbn/apm-data-access-plugin/server/utils/utility_types';
1212
import {
1313
AGENT_NAME,
1414
AT_TIMESTAMP,

x-pack/solutions/observability/plugins/apm/server/routes/services/get_service_metadata_details.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import { rangeQuery } from '@kbn/observability-plugin/server';
99
import { ProcessorEvent } from '@kbn/observability-plugin/common';
1010
import { unflattenKnownApmEventFields } from '@kbn/apm-data-access-plugin/server/utils';
11-
import type { FlattenedApmEvent } from '@kbn/apm-data-access-plugin/server/utils/unflatten_known_fields';
11+
import type { FlattenedApmEvent } from '@kbn/apm-data-access-plugin/server/utils/utility_types';
1212
import { getAgentName } from '@kbn/elastic-agent-utils';
1313
import type { SortOptions } from '@elastic/elasticsearch/lib/api/types';
1414
import { environmentQuery } from '../../../common/utils/environment_query';

x-pack/solutions/observability/plugins/apm/server/routes/services/get_service_metadata_icons.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import { rangeQuery } from '@kbn/observability-plugin/server';
99
import { ProcessorEvent } from '@kbn/observability-plugin/common';
1010
import { unflattenKnownApmEventFields } from '@kbn/apm-data-access-plugin/server/utils';
11-
import type { FlattenedApmEvent } from '@kbn/apm-data-access-plugin/server/utils/unflatten_known_fields';
11+
import type { FlattenedApmEvent } from '@kbn/apm-data-access-plugin/server/utils/utility_types';
1212
import { getAgentName } from '@kbn/elastic-agent-utils';
1313
import { maybe } from '../../../common/utils/maybe';
1414
import { asMutableArray } from '../../../common/utils/as_mutable_array';

x-pack/solutions/observability/plugins/apm/server/routes/traces/get_unified_trace_items.ts

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
import type { Sort } from '@elastic/elasticsearch/lib/api/types';
99
import type { APMEventClient } from '@kbn/apm-data-access-plugin/server';
10-
import { unflattenKnownApmEventFields } from '@kbn/apm-data-access-plugin/server/utils';
10+
import { accessKnownApmEventFields } from '@kbn/apm-data-access-plugin/server/utils';
1111
import { ProcessorEvent } from '@kbn/observability-plugin/common';
1212
import { rangeQuery, termQuery } from '@kbn/observability-plugin/server';
1313
import type { APMConfig } from '../..';
@@ -154,28 +154,27 @@ export async function getUnifiedTraceItems({
154154
return {
155155
traceItems: unifiedTraceItems.hits.hits
156156
.map((hit) => {
157-
const event = unflattenKnownApmEventFields(hit.fields, fields);
158-
const apmDuration = event.span?.duration?.us || event.transaction?.duration?.us;
159-
const id = event.span?.id || event.transaction?.id;
160-
if (!id) {
157+
const event = accessKnownApmEventFields(hit.fields, fields);
158+
const apmDuration = event[SPAN_DURATION] ?? event[TRANSACTION_DURATION];
159+
const id = event[SPAN_ID] ?? event[TRANSACTION_ID];
160+
const name = event[SPAN_NAME] ?? event[TRANSACTION_NAME];
161+
162+
if (!id || !name) {
161163
return undefined;
162164
}
163165

164166
const docErrorCount = errorCountByDocId[id] || 0;
167+
const statusCode = event[STATUS_CODE];
165168
return {
166-
id: event.span?.id ?? event.transaction?.id,
167-
timestampUs: event.timestamp?.us ?? toMicroseconds(event[AT_TIMESTAMP]),
168-
name: event.span?.name ?? event.transaction?.name,
169-
traceId: event.trace.id,
170-
duration: resolveDuration(apmDuration, event.duration),
171-
hasError:
172-
docErrorCount > 0 ||
173-
(event.status?.code && Array.isArray(event.status.code)
174-
? event.status.code[0] === 'Error'
175-
: false),
176-
parentId: event.parent?.id,
177-
serviceName: event.service.name,
178-
} as TraceItem;
169+
id,
170+
name,
171+
timestampUs: event[TIMESTAMP_US] ?? toMicroseconds(event[AT_TIMESTAMP]),
172+
traceId: event[TRACE_ID],
173+
duration: resolveDuration(apmDuration, event[DURATION]),
174+
hasError: docErrorCount > 0 || statusCode === 'Error',
175+
parentId: event[PARENT_ID],
176+
serviceName: event['service.name'],
177+
} satisfies TraceItem;
179178
})
180179
.filter((_) => _) as TraceItem[],
181180
unifiedTraceErrors,

x-pack/solutions/observability/plugins/apm/server/routes/transactions/get_span/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import { rangeQuery, termQuery } from '@kbn/observability-plugin/server';
99
import { ProcessorEvent } from '@kbn/observability-plugin/common';
1010
import { unflattenKnownApmEventFields } from '@kbn/apm-data-access-plugin/server/utils';
11-
import type { FlattenedApmEvent } from '@kbn/apm-data-access-plugin/server/utils/unflatten_known_fields';
11+
import type { FlattenedApmEvent } from '@kbn/apm-data-access-plugin/server/utils/utility_types';
1212
import { merge, omit } from 'lodash';
1313
import { maybe } from '../../../../common/utils/maybe';
1414
import { SPAN_ID, SPAN_STACKTRACE, TRACE_ID } from '../../../../common/es_fields/apm';

x-pack/solutions/observability/plugins/apm_data_access/server/utils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,4 @@ export {
1616

1717
export { withApmSpan } from './utils/with_apm_span';
1818
export { unflattenKnownApmEventFields } from './utils/unflatten_known_fields';
19+
export { accessKnownApmEventFields } from './utils/access_known_fields';
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import { accessKnownApmEventFields } from './access_known_fields';
9+
import type { FlattenedApmEvent } from './utility_types';
10+
11+
describe('accessKnownApmEventFields', () => {
12+
const input = {
13+
'@timestamp': ['2024-10-10T10:10:10.000Z'],
14+
'service.name': ['node-svc'],
15+
'service.version': ['1.0.0'],
16+
'service.environment': ['production'],
17+
'agent.name': ['nodejs'],
18+
'links.span_id': ['link1', 'link2'],
19+
};
20+
21+
it('should return either single or array values for the various known field types', () => {
22+
const event = accessKnownApmEventFields(input as Partial<FlattenedApmEvent>, [
23+
'@timestamp',
24+
'service.name',
25+
]);
26+
27+
expect(event['service.name']).not.toBeUndefined();
28+
expect(event['agent.name']).toBe('nodejs');
29+
expect(event['agent.version']).toBeUndefined();
30+
expect(event['links.span_id']).toEqual(['link1', 'link2']);
31+
expect(event['links.trace_id']).toBeUndefined();
32+
});
33+
34+
it('should validate all required fields are present in the input document', () => {
35+
expect(() => accessKnownApmEventFields(input, ['@timestamp', 'service.name'])).not.toThrow();
36+
37+
expect(() => accessKnownApmEventFields({}, ['@timestamp', 'service.name'])).toThrowError(
38+
'Missing required fields (@timestamp, service.name) in event'
39+
);
40+
41+
expect(() =>
42+
accessKnownApmEventFields({ ...input, 'service.name': [] }, ['@timestamp', 'service.name'])
43+
).toThrowError('Missing required fields (service.name) in event');
44+
});
45+
46+
it('exposes an `unflatten` method', () => {
47+
const smallInput = {
48+
'@timestamp': ['2024-10-10T10:10:10.000Z'],
49+
'service.name': ['node-svc'],
50+
'links.span_id': ['link1', 'link2'],
51+
};
52+
53+
const event = accessKnownApmEventFields(smallInput, ['@timestamp', 'service.name']);
54+
55+
expect(typeof event.unflatten).toBe('function');
56+
57+
const unflattened = event.unflatten();
58+
59+
expect(unflattened).toEqual({
60+
'@timestamp': '2024-10-10T10:10:10.000Z',
61+
service: { name: 'node-svc' },
62+
links: { span_id: ['link1', 'link2'] },
63+
});
64+
});
65+
66+
it('prevents mutations on the original object', () => {
67+
const smallInput = {
68+
'@timestamp': ['2024-10-10T10:10:10.000Z'],
69+
'service.name': ['node-svc'],
70+
'links.span_id': ['link1', 'link2'],
71+
};
72+
73+
const event = accessKnownApmEventFields(smallInput as Partial<FlattenedApmEvent>);
74+
75+
// The proxied object is immutable. It will prevent mutations and will throw a TypeError
76+
expect(() => {
77+
// Disabling type checking here to ensure we also have runtime protection of immutability,
78+
// so people can't just cast their way out of this. Normally, doing the setter op will
79+
// error at compile time.
80+
// @ts-ignore
81+
event['agent.name'] = 'nodejs';
82+
}).toThrowError("'set' on proxy: trap returned falsish for property 'agent.name'");
83+
});
84+
});
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import { unflattenKnownApmEventFields } from './unflatten_known_fields';
9+
import {
10+
type UnflattenedKnownFields,
11+
type FlattenedApmEvent,
12+
type MapToSingleOrMultiValue,
13+
KNOWN_SINGLE_VALUED_FIELDS_SET,
14+
} from './utility_types';
15+
16+
type RequiredApmFields<
17+
T extends Partial<FlattenedApmEvent>,
18+
R extends keyof FlattenedApmEvent = never
19+
> = Partial<T> & Required<Pick<T, R>>;
20+
21+
/**
22+
* A Proxied APM document that is strongly typed and runtime checked to be correct.
23+
* Accessing fields from the document will correctly return single or multi values
24+
* according to known field types.
25+
*/
26+
type ProxiedApmEvent<
27+
T extends Partial<FlattenedApmEvent>,
28+
R extends keyof FlattenedApmEvent = never
29+
> = Readonly<MapToSingleOrMultiValue<RequiredApmFields<T, R>>>;
30+
31+
/**
32+
* Interface for exposing an `unflatten()` method on proxied APM documents.
33+
*/
34+
interface UnflattenApmDocument<
35+
T extends Partial<FlattenedApmEvent>,
36+
R extends keyof FlattenedApmEvent = never
37+
> {
38+
/**
39+
* Unflattens the APM Event, so fields can be accessed via `event.service?.name`.
40+
*
41+
* ```ts
42+
* const unflattened = accessKnownApmEventFields(hit, ['service.name']).unflatten();
43+
*
44+
* console.log(unflattened.service.name); // outputs "node-svc" for example
45+
* ```
46+
*/
47+
unflatten(): UnflattenedKnownFields<RequiredApmFields<T, R>>;
48+
}
49+
50+
/**
51+
* An APM document that is strongly typed and runtime checked to be correct.
52+
* Accessing fields from the document will correctly return single or multi values
53+
* according to known field types.
54+
*
55+
* An `unflatten()` method is also exposed by this document to return an unflattened
56+
* version of the document.
57+
*/
58+
type ApmDocument<
59+
T extends Partial<FlattenedApmEvent>,
60+
R extends keyof FlattenedApmEvent = never
61+
> = ProxiedApmEvent<T, R> & UnflattenApmDocument<T, R>;
62+
63+
/**
64+
* Validates an APM Event document, checking if it has all the required fields if provided,
65+
* returning a proxied version of the document to allow strongly typed access to known single
66+
* or multi-value fields. The proxy also exposes an `unflatten()` method to return an unflattened
67+
* version of the document.
68+
*
69+
* ## Example
70+
*
71+
* ```ts
72+
* const event = accessKnownApmEventFields(hit, ['service.name']);
73+
*
74+
* // The key is strongly typed to be `keyof FlattenedApmEvent`.
75+
* console.log(event['service.name']) // => outputs `"node-svc"`, not `["node-svc"]` as in the original doc
76+
*
77+
* const unflattened = event.unflatten();
78+
*
79+
* console.log(unflattened.service.name); // => outputs "node-svc" like above
80+
*/
81+
export function accessKnownApmEventFields<
82+
T extends Partial<FlattenedApmEvent>,
83+
R extends keyof FlattenedApmEvent = never
84+
>(fields: T, required?: R[]): ApmDocument<T, R>;
85+
86+
export function accessKnownApmEventFields(fields: Record<string, any>, required?: string[]) {
87+
if (required) {
88+
const missingRequiredFields = required.filter((key) => {
89+
const value = fields[key];
90+
91+
return value == null || (Array.isArray(value) && value.length === 0);
92+
});
93+
94+
if (missingRequiredFields.length) {
95+
throw new Error(`Missing required fields (${missingRequiredFields.join(', ')}) in event`);
96+
}
97+
}
98+
99+
return new Proxy(fields, accessHandler);
100+
}
101+
102+
const accessHandler = {
103+
get(fields: Record<string, any>, key: string) {
104+
if (key === 'unflatten') {
105+
return () => unflattenKnownApmEventFields(fields);
106+
}
107+
108+
const value = fields[key];
109+
110+
return KNOWN_SINGLE_VALUED_FIELDS_SET.has(key) && Array.isArray(value) ? value[0] : value;
111+
},
112+
113+
// Trap any setters to make the proxied object immutable.
114+
set() {
115+
return false;
116+
},
117+
};

0 commit comments

Comments
 (0)