utils.tsx 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640
  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 {removeFilterMaskedEntries} from 'sentry/components/events/interfaces/utils';
  14. import StructuredEventData from 'sentry/components/structuredEventData';
  15. import {t} from 'sentry/locale';
  16. import plugins from 'sentry/plugins';
  17. import {space} from 'sentry/styles/space';
  18. import type {Event} from 'sentry/types/event';
  19. import type {KeyValueListData, KeyValueListDataItem} from 'sentry/types/group';
  20. import type {Organization} from 'sentry/types/organization';
  21. import type {Project} from 'sentry/types/project';
  22. import type {AvatarUser} from 'sentry/types/user';
  23. import {defined} from 'sentry/utils';
  24. import commonTheme from 'sentry/utils/theme';
  25. import {AppEventContext, getKnownAppContextData, getUnknownAppContextData} from './app';
  26. import {
  27. BrowserEventContext,
  28. getKnownBrowserContextData,
  29. getUnknownBrowserContextData,
  30. } from './browser';
  31. import {DefaultContext, getDefaultContextData} from './default';
  32. import {
  33. DeviceEventContext,
  34. getKnownDeviceContextData,
  35. getUnknownDeviceContextData,
  36. } from './device';
  37. import {getKnownGpuContextData, getUnknownGpuContextData, GPUEventContext} from './gpu';
  38. import {
  39. getKnownMemoryInfoContextData,
  40. getUnknownMemoryInfoContextData,
  41. MemoryInfoEventContext,
  42. } from './memoryInfo';
  43. import {
  44. getKnownOperatingSystemContextData,
  45. getUnknownOperatingSystemContextData,
  46. OperatingSystemEventContext,
  47. } from './operatingSystem';
  48. import {
  49. getKnownPlatformContextData,
  50. getPlatformContextIcon,
  51. getUnknownPlatformContextData,
  52. KNOWN_PLATFORM_CONTEXTS,
  53. } from './platform';
  54. import {
  55. getKnownProfileContextData,
  56. getUnknownProfileContextData,
  57. ProfileEventContext,
  58. } from './profile';
  59. import {getReduxContextData, ReduxContext} from './redux';
  60. import {
  61. getKnownReplayContextData,
  62. getUnknownReplayContextData,
  63. ReplayEventContext,
  64. } from './replay';
  65. import {
  66. getKnownRuntimeContextData,
  67. getUnknownRuntimeContextData,
  68. RuntimeEventContext,
  69. } from './runtime';
  70. import {
  71. getKnownStateContextData,
  72. getUnknownStateContextData,
  73. StateEventContext,
  74. } from './state';
  75. import {
  76. getKnownThreadPoolInfoContextData,
  77. getUnknownThreadPoolInfoContextData,
  78. ThreadPoolInfoEventContext,
  79. } from './threadPoolInfo';
  80. import {
  81. getKnownTraceContextData,
  82. getUnknownTraceContextData,
  83. TraceEventContext,
  84. } from './trace';
  85. import {
  86. getKnownUnityContextData,
  87. getUnknownUnityContextData,
  88. UnityEventContext,
  89. } from './unity';
  90. import {
  91. getKnownUserContextData,
  92. getUnknownUserContextData,
  93. UserEventContext,
  94. } from './user';
  95. const CONTEXT_TYPES = {
  96. default: DefaultContext,
  97. app: AppEventContext,
  98. device: DeviceEventContext,
  99. memory_info: MemoryInfoEventContext,
  100. browser: BrowserEventContext,
  101. os: OperatingSystemEventContext,
  102. unity: UnityEventContext,
  103. runtime: RuntimeEventContext,
  104. user: UserEventContext,
  105. gpu: GPUEventContext,
  106. trace: TraceEventContext,
  107. threadpool_info: ThreadPoolInfoEventContext,
  108. state: StateEventContext,
  109. profile: ProfileEventContext,
  110. replay: ReplayEventContext,
  111. // 'redux.state' will be replaced with more generic context called 'state'
  112. 'redux.state': ReduxContext,
  113. // 'ThreadPool Info' will be replaced with 'threadpool_info' but
  114. // we want to keep it here for now so it works for existing versions
  115. 'ThreadPool Info': ThreadPoolInfoEventContext,
  116. // 'Memory Info' will be replaced with 'memory_info' but
  117. // we want to keep it here for now so it works for existing versions
  118. 'Memory Info': MemoryInfoEventContext,
  119. };
  120. /**
  121. * Generates the class name used for contexts
  122. */
  123. export function generateIconName(
  124. name?: string | boolean | null,
  125. version?: string
  126. ): string {
  127. if (!defined(name) || typeof name === 'boolean') {
  128. return '';
  129. }
  130. const lowerCaseName = name.toLowerCase();
  131. // amazon fire tv device id changes with version: AFTT, AFTN, AFTS, AFTA, AFTVA (alexa), ...
  132. if (lowerCaseName.startsWith('aft')) {
  133. return 'amazon';
  134. }
  135. if (lowerCaseName.startsWith('sm-') || lowerCaseName.startsWith('st-')) {
  136. return 'samsung';
  137. }
  138. if (lowerCaseName.startsWith('moto')) {
  139. return 'motorola';
  140. }
  141. if (lowerCaseName.startsWith('pixel')) {
  142. return 'google';
  143. }
  144. const formattedName = name
  145. .split(/\d/)[0]
  146. .toLowerCase()
  147. .replace(/[^a-z0-9\-]+/g, '-')
  148. .replace(/\-+$/, '')
  149. .replace(/^\-+/, '');
  150. if (formattedName === 'edge' && version) {
  151. const majorVersion = version.split('.')[0];
  152. const isLegacyEdge = majorVersion >= '12' && majorVersion <= '18';
  153. return isLegacyEdge ? 'legacy-edge' : 'edge';
  154. }
  155. if (formattedName.endsWith('-mobile')) {
  156. return formattedName.split('-')[0];
  157. }
  158. return formattedName;
  159. }
  160. export function getContextComponent(type: string) {
  161. return CONTEXT_TYPES[type] || plugins.contexts[type] || CONTEXT_TYPES.default;
  162. }
  163. export function getSourcePlugin(pluginContexts: Array<any>, contextType: string) {
  164. if (CONTEXT_TYPES[contextType]) {
  165. return null;
  166. }
  167. for (const plugin of pluginContexts) {
  168. if (plugin.contexts.indexOf(contextType) >= 0) {
  169. return plugin;
  170. }
  171. }
  172. return null;
  173. }
  174. export function getRelativeTimeFromEventDateCreated(
  175. eventDateCreated: string,
  176. timestamp?: string,
  177. showTimestamp = true
  178. ) {
  179. if (!defined(timestamp)) {
  180. return timestamp;
  181. }
  182. const dateTime = moment(timestamp);
  183. if (!dateTime.isValid()) {
  184. return timestamp;
  185. }
  186. const relativeTime = `(${dateTime.from(eventDateCreated, true)} ${t(
  187. 'before this event'
  188. )})`;
  189. if (!showTimestamp) {
  190. return <RelativeTime>{relativeTime}</RelativeTime>;
  191. }
  192. return (
  193. <Fragment>
  194. {timestamp}
  195. <RelativeTime>{relativeTime}</RelativeTime>
  196. </Fragment>
  197. );
  198. }
  199. export type KnownDataDetails = Omit<KeyValueListDataItem, 'key'> | undefined;
  200. export function getKnownData<Data, DataType>({
  201. data,
  202. knownDataTypes,
  203. onGetKnownDataDetails,
  204. meta,
  205. }: {
  206. data: Data;
  207. knownDataTypes: string[];
  208. onGetKnownDataDetails: (props: {data: Data; type: DataType}) => KnownDataDetails;
  209. meta?: Record<any, any>;
  210. }): KeyValueListData {
  211. const filteredTypes = knownDataTypes.filter(knownDataType => {
  212. if (
  213. typeof data[knownDataType] !== 'number' &&
  214. typeof data[knownDataType] !== 'boolean' &&
  215. !data[knownDataType]
  216. ) {
  217. return !!meta?.[knownDataType];
  218. }
  219. return true;
  220. });
  221. return filteredTypes
  222. .map(type => {
  223. const knownDataDetails = onGetKnownDataDetails({
  224. data,
  225. type: type as unknown as DataType,
  226. });
  227. if (!knownDataDetails) {
  228. return null;
  229. }
  230. return {
  231. key: type,
  232. ...knownDataDetails,
  233. value: knownDataDetails.value,
  234. };
  235. })
  236. .filter(defined);
  237. }
  238. export function getKnownStructuredData(
  239. knownData: KeyValueListData,
  240. meta: Record<string, any>
  241. ): KeyValueListData {
  242. return knownData.map(kd => ({
  243. ...kd,
  244. value: (
  245. <StructuredEventData data={kd.value} meta={meta?.[kd.key]} withAnnotatedText />
  246. ),
  247. }));
  248. }
  249. export function getUnknownData({
  250. allData,
  251. knownKeys,
  252. meta,
  253. }: {
  254. allData: Record<string, any>;
  255. knownKeys: string[];
  256. meta?: NonNullable<Event['_meta']>[keyof Event['_meta']];
  257. }): KeyValueListData {
  258. return Object.entries(allData)
  259. .filter(
  260. ([key]) =>
  261. key !== 'type' &&
  262. key !== 'title' &&
  263. !knownKeys.includes(key) &&
  264. (typeof allData[key] !== 'number' && !allData[key] ? !!meta?.[key]?.[''] : true)
  265. )
  266. .map(([key, value]) => ({
  267. key,
  268. value,
  269. subject: key,
  270. meta: meta?.[key]?.[''],
  271. }));
  272. }
  273. export function getContextTitle({
  274. alias,
  275. type,
  276. value = {},
  277. }: {
  278. alias: string;
  279. type: string;
  280. value?: Record<string, any>;
  281. }) {
  282. if (defined(value.title) && typeof value.title !== 'object') {
  283. return value.title;
  284. }
  285. if (!defined(type)) {
  286. return alias;
  287. }
  288. switch (type) {
  289. case 'app':
  290. return t('App');
  291. case 'device':
  292. return t('Device');
  293. case 'browser':
  294. return t('Browser');
  295. case 'profile':
  296. return t('Profile');
  297. case 'replay':
  298. return t('Replay');
  299. case 'response':
  300. return t('Response');
  301. case 'feedback':
  302. return t('Feedback');
  303. case 'os':
  304. return t('Operating System');
  305. case 'user':
  306. return t('User');
  307. case 'gpu':
  308. return t('Graphics Processing Unit');
  309. case 'runtime':
  310. return t('Runtime');
  311. case 'trace':
  312. return t('Trace Details');
  313. case 'otel':
  314. return 'OpenTelemetry';
  315. case 'unity':
  316. return 'Unity';
  317. case 'memory_info': // Current value for memory info
  318. case 'Memory Info': // Legacy for memory info
  319. return t('Memory Info');
  320. case 'threadpool_info': // Current value for thread pool info
  321. case 'ThreadPool Info': // Legacy value for thread pool info
  322. return t('Thread Pool Info');
  323. case 'default':
  324. switch (alias) {
  325. case 'state':
  326. return t('Application State');
  327. case 'laravel':
  328. return t('Laravel Context');
  329. case 'profile':
  330. return t('Profile');
  331. case 'replay':
  332. return t('Replay');
  333. default:
  334. return alias;
  335. }
  336. default:
  337. return type;
  338. }
  339. }
  340. export function getContextMeta(event: Event, contextType: string): Record<string, any> {
  341. const defaultMeta = event._meta?.contexts?.[contextType] ?? {};
  342. switch (contextType) {
  343. case 'memory_info': // Current
  344. case 'Memory Info': // Legacy
  345. return event._meta?.contexts?.['Memory Info'] ?? defaultMeta;
  346. case 'threadpool_info': // Current
  347. case 'ThreadPool Info': // Legacy
  348. return event._meta?.contexts?.['ThreadPool Info'] ?? defaultMeta;
  349. case 'user':
  350. return event._meta?.user ?? defaultMeta;
  351. default:
  352. return defaultMeta;
  353. }
  354. }
  355. export function getContextIcon({
  356. alias,
  357. type,
  358. value = {},
  359. contextIconProps = {},
  360. }: {
  361. alias: string;
  362. type: string;
  363. contextIconProps?: Partial<ContextIconProps>;
  364. value?: Record<string, any>;
  365. }): React.ReactNode {
  366. if (KNOWN_PLATFORM_CONTEXTS.has(alias)) {
  367. return getPlatformContextIcon({
  368. platform: alias,
  369. size: contextIconProps?.size ?? 'xl',
  370. });
  371. }
  372. let iconName = '';
  373. switch (type) {
  374. case 'device':
  375. iconName = generateIconName(value?.model);
  376. break;
  377. case 'client_os':
  378. case 'os':
  379. iconName = generateIconName(value?.name);
  380. break;
  381. case 'runtime':
  382. case 'browser':
  383. iconName = generateIconName(value?.name, value?.version);
  384. break;
  385. case 'user':
  386. const user = removeFilterMaskedEntries(value);
  387. const iconSize = commonTheme.iconNumberSizes[contextIconProps?.size ?? 'xl'];
  388. return <UserAvatar user={user as AvatarUser} size={iconSize} gravatar={false} />;
  389. case 'gpu':
  390. iconName = generateIconName(value?.vendor_name ? value?.vendor_name : value?.name);
  391. break;
  392. default:
  393. break;
  394. }
  395. if (iconName.length === 0) {
  396. return null;
  397. }
  398. const imageName = getLogoImage(iconName);
  399. if (imageName === logoUnknown) {
  400. return null;
  401. }
  402. return <ContextIcon name={iconName} {...contextIconProps} />;
  403. }
  404. export function getFormattedContextData({
  405. event,
  406. contextType,
  407. contextValue,
  408. organization,
  409. project,
  410. location,
  411. }: {
  412. contextType: string;
  413. contextValue: any;
  414. event: Event;
  415. location: Location;
  416. organization: Organization;
  417. project?: Project;
  418. }): KeyValueListData {
  419. const meta = getContextMeta(event, contextType);
  420. if (KNOWN_PLATFORM_CONTEXTS.has(contextType)) {
  421. return [
  422. ...getKnownPlatformContextData({platform: contextType, data: contextValue, meta}),
  423. ...getUnknownPlatformContextData({platform: contextType, data: contextValue, meta}),
  424. ];
  425. }
  426. switch (contextType) {
  427. case 'app':
  428. return [
  429. ...getKnownAppContextData({data: contextValue, event, meta}),
  430. ...getUnknownAppContextData({data: contextValue, meta}),
  431. ];
  432. case 'device':
  433. return [
  434. ...getKnownDeviceContextData({data: contextValue, event, meta}),
  435. ...getUnknownDeviceContextData({data: contextValue, meta}),
  436. ];
  437. case 'memory_info': // Current
  438. case 'Memory Info': // Legacy
  439. return [
  440. ...getKnownMemoryInfoContextData({data: contextValue, event, meta}),
  441. ...getUnknownMemoryInfoContextData({data: contextValue, meta}),
  442. ];
  443. case 'browser':
  444. return [
  445. ...getKnownBrowserContextData({data: contextValue, meta}),
  446. ...getUnknownBrowserContextData({data: contextValue, meta}),
  447. ];
  448. case 'os':
  449. return [
  450. ...getKnownOperatingSystemContextData({data: contextValue, meta}),
  451. ...getUnknownOperatingSystemContextData({data: contextValue, meta}),
  452. ];
  453. case 'unity':
  454. return [
  455. ...getKnownUnityContextData({data: contextValue, meta}),
  456. ...getUnknownUnityContextData({data: contextValue, meta}),
  457. ];
  458. case 'runtime':
  459. return [
  460. ...getKnownRuntimeContextData({data: contextValue, meta}),
  461. ...getUnknownRuntimeContextData({data: contextValue, meta}),
  462. ];
  463. case 'user':
  464. return [
  465. ...getKnownUserContextData({data: contextValue, meta}),
  466. ...getUnknownUserContextData({data: contextValue, meta}),
  467. ];
  468. case 'gpu':
  469. return [
  470. ...getKnownGpuContextData({data: contextValue, meta}),
  471. ...getUnknownGpuContextData({data: contextValue, meta}),
  472. ];
  473. case 'trace':
  474. return [
  475. ...getKnownTraceContextData({
  476. data: contextValue,
  477. event,
  478. meta,
  479. organization,
  480. location,
  481. }),
  482. ...getUnknownTraceContextData({data: contextValue, meta}),
  483. ];
  484. case 'threadpool_info': // Current
  485. case 'ThreadPool Info': // Legacy
  486. return [
  487. ...getKnownThreadPoolInfoContextData({data: contextValue, event, meta}),
  488. ...getUnknownThreadPoolInfoContextData({data: contextValue, meta}),
  489. ];
  490. case 'redux.state':
  491. return getReduxContextData({data: contextValue});
  492. case 'state':
  493. return [
  494. ...getKnownStateContextData({data: contextValue, meta}),
  495. ...getUnknownStateContextData({data: contextValue, meta}),
  496. ];
  497. case 'profile':
  498. return [
  499. ...getKnownProfileContextData({data: contextValue, meta, organization, project}),
  500. ...getUnknownProfileContextData({data: contextValue, meta}),
  501. ];
  502. case 'replay':
  503. return [
  504. ...getKnownReplayContextData({data: contextValue, meta, organization}),
  505. ...getUnknownReplayContextData({data: contextValue, meta}),
  506. ];
  507. default:
  508. return getDefaultContextData(contextValue);
  509. }
  510. }
  511. /**
  512. * Reimplemented as util function from legacy summaries deleted in this PR - https://github.com/getsentry/sentry/pull/71695/files
  513. * Consildated into one function and neglects any meta annotations since those will be rendered in the proper contexts section.
  514. * The only difference is we don't render 'unknown' values, since that doesn't help the user.
  515. */
  516. export function getContextSummary({
  517. type,
  518. value: data,
  519. }: {
  520. type: string;
  521. value?: Record<string, any>;
  522. }): {
  523. subtitle: React.ReactNode;
  524. title: React.ReactNode;
  525. } {
  526. let title: React.ReactNode = null;
  527. let subtitle: React.ReactNode = null;
  528. switch (type) {
  529. case 'device':
  530. title = (
  531. <DeviceName value={data?.model ?? ''}>
  532. {deviceName => <span>{deviceName ? deviceName : data?.name}</span>}
  533. </DeviceName>
  534. );
  535. if (defined(data?.arch)) {
  536. subtitle = t('Arch: ') + data?.arch;
  537. } else if (defined(data?.model)) {
  538. subtitle = t('Model: ') + data?.model;
  539. }
  540. break;
  541. case 'gpu':
  542. title = data?.name ?? null;
  543. if (defined(data?.vendor_name)) {
  544. subtitle = t('Vendor: ') + data?.vendor_name;
  545. }
  546. break;
  547. case 'os':
  548. case 'client_os':
  549. title = data?.name ?? null;
  550. if (defined(data?.version) && typeof data?.version === 'string') {
  551. subtitle = t('Version: ') + data?.version;
  552. } else if (defined(data?.kernel_version)) {
  553. subtitle = t('Kernel: ') + data?.kernel_version;
  554. }
  555. break;
  556. case 'user':
  557. if (defined(data?.email)) {
  558. title = data?.email;
  559. }
  560. if (defined(data?.ip_address) && !title) {
  561. title = data?.ip_address;
  562. }
  563. if (defined(data?.id)) {
  564. title = title ? title : data?.id;
  565. subtitle = t('ID: ') + data?.id;
  566. }
  567. if (defined(data?.username)) {
  568. title = title ? title : data?.username;
  569. subtitle = t('Username: ') + data?.username;
  570. }
  571. if (title === subtitle) {
  572. return {
  573. title,
  574. subtitle: null,
  575. };
  576. }
  577. break;
  578. case 'runtime':
  579. case 'browser':
  580. title = data?.name ?? null;
  581. if (defined(data?.version)) {
  582. subtitle = t('Version: ') + data?.version;
  583. }
  584. break;
  585. default:
  586. break;
  587. }
  588. return {
  589. title,
  590. subtitle,
  591. };
  592. }
  593. const RelativeTime = styled('span')`
  594. color: ${p => p.theme.subText};
  595. margin-left: ${space(0.5)};
  596. `;
  597. export const CONTEXT_DOCS_LINK = `https://docs.sentry.io/platform-redirect/?next=/enriching-events/context/`;