From 1c56c2171457ba65a7959d1d09cde5841144f110 Mon Sep 17 00:00:00 2001 From: Ryan Albrecht Date: Mon, 19 May 2025 11:36:58 -0700 Subject: [PATCH 1/2] fix(replay): Expand replay user.geo tag if an object is found --- static/app/utils/replays/replayDataUtils.tsx | 23 +++++++++++++++----- static/app/views/replays/types.tsx | 10 +++++++++ 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/static/app/utils/replays/replayDataUtils.tsx b/static/app/utils/replays/replayDataUtils.tsx index c39ca8e8170008..73c5c167e9d6c3 100644 --- a/static/app/utils/replays/replayDataUtils.tsx +++ b/static/app/utils/replays/replayDataUtils.tsx @@ -1,4 +1,5 @@ import invariant from 'invariant'; +import isPlainObject from 'lodash/isPlainObject'; import {duration} from 'moment-timezone'; import {deviceNameMapper} from 'sentry/components/deviceName'; @@ -10,13 +11,23 @@ const defaultValues = { has_viewed: false, }; -export function mapResponseToReplayRecord(apiResponse: any): ReplayRecord { - // Marshal special fields into tags - const user = Object.fromEntries( - Object.entries(apiResponse.user) +function mapUser(user: any): Record { + return Object.fromEntries( + Object.entries(user) .filter(([key, value]) => key !== 'display_name' && value) - .map(([key, value]) => [`user.${key}`, [value]]) + .flatMap(([key, value]) => { + if (isPlainObject(value)) { + return Object.entries(value as Record).map( + ([subKey, subValue]) => [`user.${key}.${subKey}`, [String(subValue)]] + ); + } + return [[`user.${key}`, [String(value)]]]; + }) ); +} + +export function mapResponseToReplayRecord(apiResponse: any): ReplayRecord { + // Marshal special fields into tags const unorderedTags: ReplayRecord['tags'] = { ...apiResponse.tags, ...(apiResponse.browser?.name ? {'browser.name': [apiResponse.browser.name]} : {}), @@ -40,7 +51,7 @@ export function mapResponseToReplayRecord(apiResponse: any): ReplayRecord { ...(apiResponse.os?.version ? {'os.version': [apiResponse.os.version]} : {}), ...(apiResponse.sdk?.name ? {'sdk.name': [apiResponse.sdk.name]} : {}), ...(apiResponse.sdk?.version ? {'sdk.version': [apiResponse.sdk.version]} : {}), - ...user, + ...mapUser(apiResponse.user ?? {}), }; const startedAt = new Date(apiResponse.started_at); diff --git a/static/app/views/replays/types.tsx b/static/app/views/replays/types.tsx index 0697e5a41d6549..710bd4921d1f22 100644 --- a/static/app/views/replays/types.tsx +++ b/static/app/views/replays/types.tsx @@ -1,5 +1,14 @@ import type {Duration} from 'moment-timezone'; +type Geo = Record; +// Geo could be this, not sure: +// { +// city?: string; +// country_code?: string; +// region?: string; +// subdivision?: string; +// }; + // Keep this in sync with the backend blueprint // "ReplayRecord" is distinct from the common: "replay = new ReplayReader()" export type ReplayRecord = { @@ -78,6 +87,7 @@ export type ReplayRecord = { id: null | string; ip: null | string; username: null | string; + geo?: Geo; }; /** * The number of dead clicks associated with the replay. From 47c641319a0ebafb63a86cce06138ee516687819 Mon Sep 17 00:00:00 2001 From: Ryan Albrecht Date: Tue, 20 May 2025 09:42:33 -0700 Subject: [PATCH 2/2] reuse ReplayRecord[tags] type --- static/app/utils/replays/replayDataUtils.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/app/utils/replays/replayDataUtils.tsx b/static/app/utils/replays/replayDataUtils.tsx index 73c5c167e9d6c3..47b72e0ef724de 100644 --- a/static/app/utils/replays/replayDataUtils.tsx +++ b/static/app/utils/replays/replayDataUtils.tsx @@ -11,7 +11,7 @@ const defaultValues = { has_viewed: false, }; -function mapUser(user: any): Record { +function mapUser(user: any): ReplayRecord['tags'] { return Object.fromEntries( Object.entries(user) .filter(([key, value]) => key !== 'display_name' && value)