utils.tsx 16 KB

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