utils.tsx 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553
  1. import {Fragment} from 'react';
  2. import styled from '@emotion/styled';
  3. import type {Location} from 'history';
  4. import moment from 'moment-timezone';
  5. import logoUnknown from 'sentry-logos/logo-unknown.svg';
  6. import UserAvatar from 'sentry/components/avatar/userAvatar';
  7. import {DeviceName} from 'sentry/components/deviceName';
  8. import {
  9. ContextIcon,
  10. type ContextIconProps,
  11. getLogoImage,
  12. } from 'sentry/components/events/contexts/contextIcon';
  13. import {getAppContextData} from 'sentry/components/events/contexts/knownContext/app';
  14. import {getBrowserContextData} from 'sentry/components/events/contexts/knownContext/browser';
  15. import {getCloudResourceContextData} from 'sentry/components/events/contexts/knownContext/cloudResource';
  16. import {getCultureContextData} from 'sentry/components/events/contexts/knownContext/culture';
  17. import {getDeviceContextData} from 'sentry/components/events/contexts/knownContext/device';
  18. import {getGPUContextData} from 'sentry/components/events/contexts/knownContext/gpu';
  19. import {getMemoryInfoContext} from 'sentry/components/events/contexts/knownContext/memoryInfo';
  20. import {getMissingInstrumentationContextData} from 'sentry/components/events/contexts/knownContext/missingInstrumentation';
  21. import {getOperatingSystemContextData} from 'sentry/components/events/contexts/knownContext/os';
  22. import {getProfileContextData} from 'sentry/components/events/contexts/knownContext/profile';
  23. import {getReplayContextData} from 'sentry/components/events/contexts/knownContext/replay';
  24. import {getRuntimeContextData} from 'sentry/components/events/contexts/knownContext/runtime';
  25. import {getStateContextData} from 'sentry/components/events/contexts/knownContext/state';
  26. import {getThreadPoolInfoContext} from 'sentry/components/events/contexts/knownContext/threadPoolInfo';
  27. import {getTraceContextData} from 'sentry/components/events/contexts/knownContext/trace';
  28. import {getUserContextData} from 'sentry/components/events/contexts/knownContext/user';
  29. import {
  30. getPlatformContextData,
  31. getPlatformContextIcon,
  32. getPlatformContextTitle,
  33. PLATFORM_CONTEXT_KEYS,
  34. } from 'sentry/components/events/contexts/platformContext/utils';
  35. import {userContextToActor} from 'sentry/components/events/interfaces/utils';
  36. import StructuredEventData from 'sentry/components/structuredEventData';
  37. import {t} from 'sentry/locale';
  38. import {space} from 'sentry/styles/space';
  39. import type {Event} from 'sentry/types/event';
  40. import type {KeyValueListData, KeyValueListDataItem} from 'sentry/types/group';
  41. import type {Organization} from 'sentry/types/organization';
  42. import type {Project} from 'sentry/types/project';
  43. import type {AvatarUser} from 'sentry/types/user';
  44. import {defined} from 'sentry/utils';
  45. import commonTheme from 'sentry/utils/theme';
  46. /**
  47. * Generates the class name used for contexts
  48. */
  49. export function generateIconName(
  50. name?: string | boolean | null,
  51. version?: string
  52. ): string {
  53. if (!defined(name) || typeof name === 'boolean') {
  54. return '';
  55. }
  56. const lowerCaseName = name.toLowerCase();
  57. // amazon fire tv device id changes with version: AFTT, AFTN, AFTS, AFTA, AFTVA (alexa), ...
  58. if (lowerCaseName.startsWith('aft')) {
  59. return 'amazon';
  60. }
  61. if (lowerCaseName.startsWith('sm-') || lowerCaseName.startsWith('st-')) {
  62. return 'samsung';
  63. }
  64. if (lowerCaseName.startsWith('moto')) {
  65. return 'motorola';
  66. }
  67. if (lowerCaseName.startsWith('pixel')) {
  68. return 'google';
  69. }
  70. const formattedName = name
  71. .split(/\d/)[0]
  72. .toLowerCase()
  73. .replace(/[^a-z0-9\-]+/g, '-')
  74. .replace(/\-+$/, '')
  75. .replace(/^\-+/, '');
  76. if (formattedName === 'edge' && version) {
  77. const majorVersion = version.split('.')[0];
  78. const isLegacyEdge = majorVersion >= '12' && majorVersion <= '18';
  79. return isLegacyEdge ? 'legacy-edge' : 'edge';
  80. }
  81. if (formattedName.endsWith('-mobile')) {
  82. return formattedName.split('-')[0];
  83. }
  84. return formattedName;
  85. }
  86. export function getRelativeTimeFromEventDateCreated(
  87. eventDateCreated: string,
  88. timestamp?: string,
  89. showTimestamp = true
  90. ) {
  91. if (!defined(timestamp)) {
  92. return timestamp;
  93. }
  94. const dateTime = moment(timestamp);
  95. if (!dateTime.isValid()) {
  96. return timestamp;
  97. }
  98. const relativeTime = `(${dateTime.from(eventDateCreated, true)} ${t(
  99. 'before this event'
  100. )})`;
  101. if (!showTimestamp) {
  102. return <RelativeTime>{relativeTime}</RelativeTime>;
  103. }
  104. return (
  105. <Fragment>
  106. {timestamp}
  107. <RelativeTime>{relativeTime}</RelativeTime>
  108. </Fragment>
  109. );
  110. }
  111. export type KnownDataDetails = Omit<KeyValueListDataItem, 'key'> | undefined;
  112. export function getKnownData<Data, DataType>({
  113. data,
  114. knownDataTypes,
  115. onGetKnownDataDetails,
  116. meta,
  117. }: {
  118. data: Data;
  119. knownDataTypes: string[];
  120. onGetKnownDataDetails: (props: {data: Data; type: DataType}) => KnownDataDetails;
  121. meta?: Record<any, any>;
  122. }): KeyValueListData {
  123. const filteredTypes = knownDataTypes.filter(knownDataType => {
  124. if (
  125. typeof data[knownDataType] !== 'number' &&
  126. typeof data[knownDataType] !== 'boolean' &&
  127. !data[knownDataType]
  128. ) {
  129. return !!meta?.[knownDataType];
  130. }
  131. return true;
  132. });
  133. return filteredTypes
  134. .map(type => {
  135. const knownDataDetails = onGetKnownDataDetails({
  136. data,
  137. type: type as unknown as DataType,
  138. });
  139. if (!knownDataDetails) {
  140. return null;
  141. }
  142. return {
  143. key: type,
  144. ...knownDataDetails,
  145. value: knownDataDetails.value,
  146. };
  147. })
  148. .filter(defined);
  149. }
  150. export function getKnownStructuredData(
  151. knownData: KeyValueListData,
  152. meta: Record<string, any>
  153. ): KeyValueListData {
  154. return knownData.map(kd => ({
  155. ...kd,
  156. value: (
  157. <StructuredEventData data={kd.value} meta={meta?.[kd.key]} withAnnotatedText />
  158. ),
  159. }));
  160. }
  161. export function getUnknownData({
  162. allData,
  163. knownKeys,
  164. meta,
  165. }: {
  166. allData: Record<string, any>;
  167. knownKeys: string[];
  168. meta?: NonNullable<Event['_meta']>[keyof Event['_meta']];
  169. }): KeyValueListData {
  170. return Object.entries(allData)
  171. .filter(
  172. ([key]) =>
  173. key !== 'type' &&
  174. key !== 'title' &&
  175. !knownKeys.includes(key) &&
  176. (typeof allData[key] !== 'number' && !allData[key] ? !!meta?.[key]?.[''] : true)
  177. )
  178. .map(([key, value]) => ({
  179. key,
  180. value,
  181. subject: key,
  182. meta: meta?.[key]?.[''],
  183. }));
  184. }
  185. /**
  186. * Returns the type of a given context, after coercing from its type and alias.
  187. * - 'type' refers the the `type` key on it's data blob. This is usually overridden by the SDK for known types, but not always.
  188. * - 'alias' refers to the key on event.contexts. This can be set by the user, but we have to depend on it for some contexts.
  189. */
  190. export function getContextType({alias, type}: {alias: string; type?: string}): string {
  191. if (!defined(type)) {
  192. return alias;
  193. }
  194. return type === 'default' ? alias : type;
  195. }
  196. /**
  197. * Omit certain keys from ever being displayed on context items.
  198. * All custom context (and some known context) has the type:default so we remove it.
  199. */
  200. export function getContextKeys({
  201. data,
  202. hiddenKeys = [],
  203. }: {
  204. data: Record<string, any>;
  205. hiddenKeys?: string[];
  206. }): string[] {
  207. const hiddenKeySet = new Set(hiddenKeys);
  208. return Object.keys(data).filter(
  209. ctxKey => ctxKey !== 'type' && !hiddenKeySet.has(ctxKey)
  210. );
  211. }
  212. export function getContextTitle({
  213. alias,
  214. type,
  215. value = {},
  216. }: {
  217. alias: string;
  218. type: string;
  219. value?: Record<string, any>;
  220. }) {
  221. if (defined(value.title) && typeof value.title !== 'object') {
  222. return value.title;
  223. }
  224. const contextType = getContextType({alias, type});
  225. if (PLATFORM_CONTEXT_KEYS.has(contextType)) {
  226. return getPlatformContextTitle({platform: alias});
  227. }
  228. switch (contextType) {
  229. case 'app':
  230. return t('App');
  231. case 'device':
  232. return t('Device');
  233. case 'browser':
  234. return t('Browser');
  235. case 'response':
  236. return t('Response');
  237. case 'feedback':
  238. return t('Feedback');
  239. case 'os':
  240. return t('Operating System');
  241. case 'user':
  242. return t('User');
  243. case 'gpu':
  244. return t('Graphics Processing Unit');
  245. case 'runtime':
  246. return t('Runtime');
  247. case 'trace':
  248. return t('Trace Details');
  249. case 'otel':
  250. return 'OpenTelemetry';
  251. case 'cloud_resource':
  252. return t('Cloud Resource');
  253. case 'culture':
  254. case 'Current Culture':
  255. return t('Culture');
  256. case 'missing_instrumentation':
  257. return t('Missing OTEL Instrumentation');
  258. case 'unity':
  259. return 'Unity';
  260. case 'memory_info': // Current value for memory info
  261. case 'Memory Info': // Legacy for memory info
  262. return t('Memory Info');
  263. case 'threadpool_info': // Current value for thread pool info
  264. case 'ThreadPool Info': // Legacy value for thread pool info
  265. return t('Thread Pool Info');
  266. case 'state':
  267. return t('Application State');
  268. case 'laravel':
  269. return t('Laravel Context');
  270. case 'profile':
  271. return t('Profile');
  272. case 'replay':
  273. return t('Replay');
  274. default:
  275. return contextType;
  276. }
  277. }
  278. export function getContextMeta(event: Event, contextType: string): Record<string, any> {
  279. const defaultMeta = event._meta?.contexts?.[contextType] ?? {};
  280. switch (contextType) {
  281. case 'memory_info': // Current
  282. case 'Memory Info': // Legacy
  283. return event._meta?.contexts?.['Memory Info'] ?? defaultMeta;
  284. case 'threadpool_info': // Current
  285. case 'ThreadPool Info': // Legacy
  286. return event._meta?.contexts?.['ThreadPool Info'] ?? defaultMeta;
  287. case 'user':
  288. return event._meta?.user ?? defaultMeta;
  289. default:
  290. return defaultMeta;
  291. }
  292. }
  293. export function getContextIcon({
  294. alias,
  295. type,
  296. value = {},
  297. contextIconProps = {},
  298. }: {
  299. alias: string;
  300. type: string;
  301. contextIconProps?: Partial<ContextIconProps>;
  302. value?: Record<string, any>;
  303. }): React.ReactNode {
  304. const contextType = getContextType({alias, type});
  305. if (PLATFORM_CONTEXT_KEYS.has(contextType)) {
  306. return getPlatformContextIcon({
  307. platform: alias,
  308. size: contextIconProps?.size ?? 'xl',
  309. });
  310. }
  311. let iconName = '';
  312. switch (type) {
  313. case 'device':
  314. iconName = generateIconName(value?.model);
  315. break;
  316. case 'client_os':
  317. case 'os':
  318. iconName = generateIconName(value?.name);
  319. break;
  320. case 'runtime':
  321. case 'browser':
  322. iconName = generateIconName(value?.name, value?.version);
  323. break;
  324. case 'user':
  325. const user = userContextToActor(value);
  326. const iconSize = commonTheme.iconNumberSizes[contextIconProps?.size ?? 'xl'];
  327. return <UserAvatar user={user as AvatarUser} size={iconSize} gravatar={false} />;
  328. case 'gpu':
  329. iconName = generateIconName(value?.vendor_name ? value?.vendor_name : value?.name);
  330. break;
  331. default:
  332. break;
  333. }
  334. if (iconName.length === 0) {
  335. return null;
  336. }
  337. const imageName = getLogoImage(iconName);
  338. if (imageName === logoUnknown) {
  339. return null;
  340. }
  341. return <ContextIcon name={iconName} {...contextIconProps} />;
  342. }
  343. export function getFormattedContextData({
  344. event,
  345. contextType,
  346. contextValue,
  347. organization,
  348. project,
  349. location,
  350. }: {
  351. contextType: string;
  352. contextValue: any;
  353. event: Event;
  354. location: Location;
  355. organization: Organization;
  356. project?: Project;
  357. }): KeyValueListData {
  358. const meta = getContextMeta(event, contextType);
  359. if (PLATFORM_CONTEXT_KEYS.has(contextType)) {
  360. return getPlatformContextData({platform: contextType, data: contextValue});
  361. }
  362. switch (contextType) {
  363. case 'app':
  364. return getAppContextData({data: contextValue, event, meta});
  365. case 'device':
  366. return getDeviceContextData({data: contextValue, event, meta});
  367. case 'memory_info': // Current
  368. case 'Memory Info': // Legacy
  369. return getMemoryInfoContext({data: contextValue, meta});
  370. case 'browser':
  371. return getBrowserContextData({data: contextValue, meta});
  372. case 'os':
  373. return getOperatingSystemContextData({data: contextValue, meta});
  374. case 'runtime':
  375. return getRuntimeContextData({data: contextValue, meta});
  376. case 'user':
  377. return getUserContextData({data: contextValue, meta});
  378. case 'gpu':
  379. return getGPUContextData({data: contextValue, meta});
  380. case 'trace':
  381. return getTraceContextData({
  382. data: contextValue,
  383. event,
  384. meta,
  385. organization,
  386. location,
  387. });
  388. case 'threadpool_info': // Current
  389. case 'ThreadPool Info': // Legacy
  390. return getThreadPoolInfoContext({data: contextValue, meta});
  391. case 'state':
  392. return getStateContextData({data: contextValue, meta});
  393. case 'profile':
  394. return getProfileContextData({data: contextValue, meta, organization, project});
  395. case 'replay':
  396. return getReplayContextData({data: contextValue, meta});
  397. case 'cloud_resource':
  398. return getCloudResourceContextData({data: contextValue, meta});
  399. case 'culture':
  400. case 'Current Culture':
  401. return getCultureContextData({data: contextValue, meta});
  402. case 'missing_instrumentation':
  403. return getMissingInstrumentationContextData({data: contextValue, meta});
  404. default:
  405. return getContextKeys({data: contextValue}).map(ctxKey => ({
  406. key: ctxKey,
  407. subject: ctxKey,
  408. value: contextValue[ctxKey],
  409. meta: meta?.[ctxKey]?.[''],
  410. }));
  411. }
  412. }
  413. /**
  414. * Reimplemented as util function from legacy summaries deleted in this PR - https://github.com/getsentry/sentry/pull/71695/files
  415. * Consildated into one function and neglects any meta annotations since those will be rendered in the proper contexts section.
  416. * The only difference is we don't render 'unknown' values, since that doesn't help the user.
  417. */
  418. export function getContextSummary({
  419. type,
  420. value: data,
  421. }: {
  422. type: string;
  423. value?: Record<string, any>;
  424. }): {
  425. subtitle: React.ReactNode;
  426. title: React.ReactNode;
  427. subtitleType?: string;
  428. } {
  429. let title: React.ReactNode = null;
  430. let subtitle: React.ReactNode = null;
  431. let subtitleType: string | undefined = undefined;
  432. switch (type) {
  433. case 'device':
  434. title = (
  435. <DeviceName value={data?.model ?? ''}>
  436. {deviceName => <span>{deviceName ? deviceName : data?.name}</span>}
  437. </DeviceName>
  438. );
  439. if (defined(data?.arch)) {
  440. subtitle = data?.arch;
  441. subtitleType = t('Architecture');
  442. } else if (defined(data?.model)) {
  443. subtitle = data?.model;
  444. subtitleType = t('Model');
  445. }
  446. break;
  447. case 'gpu':
  448. title = data?.name ?? null;
  449. if (defined(data?.vendor_name)) {
  450. subtitle = data?.vendor_name;
  451. subtitleType = t('Vendor');
  452. }
  453. break;
  454. case 'os':
  455. case 'client_os':
  456. title = data?.name ?? null;
  457. if (defined(data?.version) && typeof data?.version === 'string') {
  458. subtitle = data?.version;
  459. subtitleType = t('Version');
  460. } else if (defined(data?.kernel_version)) {
  461. subtitle = data?.kernel_version;
  462. subtitleType = t('Kernel');
  463. }
  464. break;
  465. case 'user':
  466. if (defined(data?.email)) {
  467. title = data?.email;
  468. }
  469. if (defined(data?.ip_address) && !title) {
  470. title = data?.ip_address;
  471. }
  472. if (defined(data?.id)) {
  473. title = title ? title : data?.id;
  474. subtitle = data?.id;
  475. subtitleType = t('ID');
  476. }
  477. if (defined(data?.username)) {
  478. title = title ? title : data?.username;
  479. subtitle = data?.username;
  480. subtitleType = t('Username');
  481. }
  482. if (title === subtitle) {
  483. return {
  484. title,
  485. subtitle: null,
  486. };
  487. }
  488. break;
  489. case 'runtime':
  490. case 'browser':
  491. title = data?.name ?? null;
  492. if (defined(data?.version)) {
  493. subtitle = data?.version;
  494. subtitleType = t('Version');
  495. }
  496. break;
  497. default:
  498. break;
  499. }
  500. return {
  501. title,
  502. subtitle,
  503. subtitleType,
  504. };
  505. }
  506. const RelativeTime = styled('span')`
  507. color: ${p => p.theme.subText};
  508. margin-left: ${space(0.5)};
  509. `;
  510. export const CONTEXT_DOCS_LINK = `https://docs.sentry.io/platform-redirect/?next=/enriching-events/context/`;