diff --git a/static/app/utils/replays/getFrameDetails.spec.tsx b/static/app/utils/replays/getFrameDetails.spec.tsx index ec343d1cd950..ec68254e3306 100644 --- a/static/app/utils/replays/getFrameDetails.spec.tsx +++ b/static/app/utils/replays/getFrameDetails.spec.tsx @@ -20,4 +20,51 @@ describe('getFrameDetails', () => { expect(getFrameDetails(frame).colorGraphicsToken).toBe(color); }); }); + + describe('defaultMapper (unknown frame types)', () => { + it('uses frame.message when it is a string', () => { + const frame: any = { + op: 'unknown.custom', + category: 'unknown.custom', + timestamp: new Date('2024-01-01T00:00:00Z').getTime() / 1000, + offsetMs: 0, + timestampMs: 0, + message: 'hello', + data: {type: 'foo'}, + }; + expect(getFrameDetails(frame).description).toBe('hello'); + }); + + it('falls back to frame.data when frame.message is a non-string object', () => { + // frame.message could be a non-string at runtime (malformed frame data). + // ?? only guards null/undefined, so without a typeof check the whole frame + // object could become the description. We guard with typeof so the plain + // object falls through to frame.data instead. + const dataPayload = {type: 'foo', level: 'info', offsetMs: 0}; + const frame: any = { + op: 'unknown.custom', + category: 'unknown.custom', + timestamp: new Date('2024-01-01T00:00:00Z').getTime() / 1000, + offsetMs: 0, + timestampMs: 0, + message: {nested: 'object'}, // non-string at runtime + data: dataPayload, + }; + // Should fall back to frame.data, not use the non-string frame.message + expect(getFrameDetails(frame).description).toBe(dataPayload); + }); + + it('returns empty string when both message and data are absent', () => { + const frame: any = { + op: 'unknown.custom', + category: 'unknown.custom', + timestamp: new Date('2024-01-01T00:00:00Z').getTime() / 1000, + offsetMs: 0, + timestampMs: 0, + message: undefined, + data: undefined, + }; + expect(getFrameDetails(frame).description).toBe(''); + }); + }); }); diff --git a/static/app/utils/replays/getFrameDetails.tsx b/static/app/utils/replays/getFrameDetails.tsx index 1145f272bc11..4109410c854c 100644 --- a/static/app/utils/replays/getFrameDetails.tsx +++ b/static/app/utils/replays/getFrameDetails.tsx @@ -468,7 +468,12 @@ const MAPPER_FOR_FRAME: Record Details> = { // JSX as components, and this one is called from event handlers. const defaultMapper = (frame: any): Details => ({ colorGraphicsToken: 'neutral', - description: frame.message ?? frame.data ?? '', + // frame.message may be a non-string object at runtime (e.g. a malformed frame). + // The ?? operator only guards null/undefined, so we guard with typeof first. + // Passing a plain object is fine: BreadcrumbItem routes it to BreadcrumbStructuredData + // which renders it as an interactive inspector via StructuredEventData. + description: + (typeof frame.message === 'string' ? frame.message : undefined) ?? frame.data ?? '', tabKey: TabKey.BREADCRUMBS, title: toTitleCase(defaultTitle(frame)), icon: ,