utils.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497
  1. import {Fragment} from 'react';
  2. import styled from '@emotion/styled';
  3. import startCase from 'lodash/startCase';
  4. import moment from 'moment-timezone';
  5. import UserAvatar from 'sentry/components/avatar/userAvatar';
  6. import ContextIcon from 'sentry/components/events/contexts/contextIcon';
  7. import {removeFilterMaskedEntries} from 'sentry/components/events/interfaces/utils';
  8. import StructuredEventData from 'sentry/components/structuredEventData';
  9. import {t} from 'sentry/locale';
  10. import plugins from 'sentry/plugins';
  11. import {space} from 'sentry/styles/space';
  12. import type {
  13. AvatarUser,
  14. Event,
  15. KeyValueListData,
  16. KeyValueListDataItem,
  17. Organization,
  18. Project,
  19. } from 'sentry/types';
  20. import {defined} from 'sentry/utils';
  21. import {toTitleCase} from 'sentry/utils/string/toTitleCase';
  22. import {AppEventContext, getKnownAppContextData, getUnknownAppContextData} from './app';
  23. import {
  24. BrowserEventContext,
  25. getKnownBrowserContextData,
  26. getUnknownBrowserContextData,
  27. } from './browser';
  28. import {DefaultContext, getDefaultContextData} from './default';
  29. import {
  30. DeviceEventContext,
  31. getKnownDeviceContextData,
  32. getUnknownDeviceContextData,
  33. } from './device';
  34. import {getKnownGpuContextData, getUnknownGpuContextData, GPUEventContext} from './gpu';
  35. import {
  36. getKnownMemoryInfoContextData,
  37. getUnknownMemoryInfoContextData,
  38. MemoryInfoEventContext,
  39. } from './memoryInfo';
  40. import {
  41. getKnownOperatingSystemContextData,
  42. getUnknownOperatingSystemContextData,
  43. OperatingSystemEventContext,
  44. } from './operatingSystem';
  45. import {
  46. getKnownProfileContextData,
  47. getUnknownProfileContextData,
  48. ProfileEventContext,
  49. } from './profile';
  50. import {getReduxContextData, ReduxContext} from './redux';
  51. import {
  52. getKnownReplayContextData,
  53. getUnknownReplayContextData,
  54. ReplayEventContext,
  55. } from './replay';
  56. import {
  57. getKnownRuntimeContextData,
  58. getUnknownRuntimeContextData,
  59. RuntimeEventContext,
  60. } from './runtime';
  61. import {
  62. getKnownStateContextData,
  63. getUnknownStateContextData,
  64. StateEventContext,
  65. } from './state';
  66. import {
  67. getKnownThreadPoolInfoContextData,
  68. getUnknownThreadPoolInfoContextData,
  69. ThreadPoolInfoEventContext,
  70. } from './threadPoolInfo';
  71. import {
  72. getKnownTraceContextData,
  73. getUnknownTraceContextData,
  74. TraceEventContext,
  75. } from './trace';
  76. import {
  77. getKnownUnityContextData,
  78. getUnknownUnityContextData,
  79. UnityEventContext,
  80. } from './unity';
  81. import {
  82. getKnownUserContextData,
  83. getUnknownUserContextData,
  84. UserEventContext,
  85. } from './user';
  86. const CONTEXT_TYPES = {
  87. default: DefaultContext,
  88. app: AppEventContext,
  89. device: DeviceEventContext,
  90. memory_info: MemoryInfoEventContext,
  91. browser: BrowserEventContext,
  92. os: OperatingSystemEventContext,
  93. unity: UnityEventContext,
  94. runtime: RuntimeEventContext,
  95. user: UserEventContext,
  96. gpu: GPUEventContext,
  97. trace: TraceEventContext,
  98. threadpool_info: ThreadPoolInfoEventContext,
  99. state: StateEventContext,
  100. profile: ProfileEventContext,
  101. replay: ReplayEventContext,
  102. // 'redux.state' will be replaced with more generic context called 'state'
  103. 'redux.state': ReduxContext,
  104. // 'ThreadPool Info' will be replaced with 'threadpool_info' but
  105. // we want to keep it here for now so it works for existing versions
  106. 'ThreadPool Info': ThreadPoolInfoEventContext,
  107. // 'Memory Info' will be replaced with 'memory_info' but
  108. // we want to keep it here for now so it works for existing versions
  109. 'Memory Info': MemoryInfoEventContext,
  110. };
  111. /**
  112. * Generates the class name used for contexts
  113. */
  114. export function generateIconName(
  115. name?: string | boolean | null,
  116. version?: string
  117. ): string {
  118. if (!defined(name) || typeof name === 'boolean') {
  119. return '';
  120. }
  121. const lowerCaseName = name.toLowerCase();
  122. // amazon fire tv device id changes with version: AFTT, AFTN, AFTS, AFTA, AFTVA (alexa), ...
  123. if (lowerCaseName.startsWith('aft')) {
  124. return 'amazon';
  125. }
  126. if (lowerCaseName.startsWith('sm-') || lowerCaseName.startsWith('st-')) {
  127. return 'samsung';
  128. }
  129. if (lowerCaseName.startsWith('moto')) {
  130. return 'motorola';
  131. }
  132. if (lowerCaseName.startsWith('pixel')) {
  133. return 'google';
  134. }
  135. const formattedName = name
  136. .split(/\d/)[0]
  137. .toLowerCase()
  138. .replace(/[^a-z0-9\-]+/g, '-')
  139. .replace(/\-+$/, '')
  140. .replace(/^\-+/, '');
  141. if (formattedName === 'edge' && version) {
  142. const majorVersion = version.split('.')[0];
  143. const isLegacyEdge = majorVersion >= '12' && majorVersion <= '18';
  144. return isLegacyEdge ? 'legacy-edge' : 'edge';
  145. }
  146. if (formattedName.endsWith('-mobile')) {
  147. return formattedName.split('-')[0];
  148. }
  149. return formattedName;
  150. }
  151. export function getContextComponent(type: string) {
  152. return CONTEXT_TYPES[type] || plugins.contexts[type] || CONTEXT_TYPES.default;
  153. }
  154. export function getSourcePlugin(pluginContexts: Array<any>, contextType: string) {
  155. if (CONTEXT_TYPES[contextType]) {
  156. return null;
  157. }
  158. for (const plugin of pluginContexts) {
  159. if (plugin.contexts.indexOf(contextType) >= 0) {
  160. return plugin;
  161. }
  162. }
  163. return null;
  164. }
  165. export function getRelativeTimeFromEventDateCreated(
  166. eventDateCreated: string,
  167. timestamp?: string,
  168. showTimestamp = true
  169. ) {
  170. if (!defined(timestamp)) {
  171. return timestamp;
  172. }
  173. const dateTime = moment(timestamp);
  174. if (!dateTime.isValid()) {
  175. return timestamp;
  176. }
  177. const relativeTime = `(${dateTime.from(eventDateCreated, true)} ${t(
  178. 'before this event'
  179. )})`;
  180. if (!showTimestamp) {
  181. return <RelativeTime>{relativeTime}</RelativeTime>;
  182. }
  183. return (
  184. <Fragment>
  185. {timestamp}
  186. <RelativeTime>{relativeTime}</RelativeTime>
  187. </Fragment>
  188. );
  189. }
  190. export type KnownDataDetails = Omit<KeyValueListDataItem, 'key'> | undefined;
  191. export function getKnownData<Data, DataType>({
  192. data,
  193. knownDataTypes,
  194. onGetKnownDataDetails,
  195. meta,
  196. }: {
  197. data: Data;
  198. knownDataTypes: string[];
  199. onGetKnownDataDetails: (props: {data: Data; type: DataType}) => KnownDataDetails;
  200. meta?: Record<any, any>;
  201. }): KeyValueListData {
  202. const filteredTypes = knownDataTypes.filter(knownDataType => {
  203. if (
  204. typeof data[knownDataType] !== 'number' &&
  205. typeof data[knownDataType] !== 'boolean' &&
  206. !data[knownDataType]
  207. ) {
  208. return !!meta?.[knownDataType];
  209. }
  210. return true;
  211. });
  212. return filteredTypes
  213. .map(type => {
  214. const knownDataDetails = onGetKnownDataDetails({
  215. data,
  216. type: type as unknown as DataType,
  217. });
  218. if (!knownDataDetails) {
  219. return null;
  220. }
  221. return {
  222. key: type,
  223. ...knownDataDetails,
  224. value: knownDataDetails.value,
  225. };
  226. })
  227. .filter(defined);
  228. }
  229. export function getKnownStructuredData(
  230. knownData: KeyValueListData,
  231. meta: Record<string, any>
  232. ): KeyValueListData {
  233. return knownData.map(kd => ({
  234. ...kd,
  235. value: (
  236. <StructuredEventData data={kd.value} meta={meta?.[kd.key]} withAnnotatedText />
  237. ),
  238. }));
  239. }
  240. export function getUnknownData({
  241. allData,
  242. knownKeys,
  243. meta,
  244. }: {
  245. allData: Record<string, any>;
  246. knownKeys: string[];
  247. meta?: NonNullable<Event['_meta']>[keyof Event['_meta']];
  248. }): KeyValueListData {
  249. return Object.entries(allData)
  250. .filter(
  251. ([key]) =>
  252. key !== 'type' &&
  253. key !== 'title' &&
  254. !knownKeys.includes(key) &&
  255. (typeof allData[key] !== 'number' && !allData[key] ? !!meta?.[key]?.[''] : true)
  256. )
  257. .map(([key, value]) => ({
  258. key,
  259. value,
  260. subject: startCase(key),
  261. meta: meta?.[key]?.[''],
  262. }));
  263. }
  264. export function getContextTitle({
  265. alias,
  266. type,
  267. value = {},
  268. }: {
  269. alias: string;
  270. type: string;
  271. value?: Record<string, any>;
  272. }) {
  273. if (defined(value.title) && typeof value.title !== 'object') {
  274. return value.title;
  275. }
  276. if (!defined(type)) {
  277. return toTitleCase(alias);
  278. }
  279. switch (type) {
  280. case 'app':
  281. return t('App');
  282. case 'device':
  283. return t('Device');
  284. case 'os':
  285. return t('Operating System');
  286. case 'user':
  287. return t('User');
  288. case 'gpu':
  289. return t('Graphics Processing Unit');
  290. case 'runtime':
  291. return t('Runtime');
  292. case 'trace':
  293. return t('Trace Details');
  294. case 'otel':
  295. return t('OpenTelemetry');
  296. case 'unity':
  297. return t('Unity');
  298. case 'memory_info': // Current value for memory info
  299. case 'Memory Info': // Legacy for memory info
  300. return t('Memory Info');
  301. case 'threadpool_info': // Current value for thread pool info
  302. case 'ThreadPool Info': // Legacy value for thread pool info
  303. return t('Thread Pool Info');
  304. case 'default':
  305. if (alias === 'state') {
  306. return t('Application State');
  307. }
  308. return toTitleCase(alias);
  309. default:
  310. return toTitleCase(type);
  311. }
  312. }
  313. export function getContextMeta(event: Event, contextType: string): Record<string, any> {
  314. const defaultMeta = event._meta?.contexts?.[contextType] ?? {};
  315. switch (contextType) {
  316. case 'memory_info': // Current
  317. case 'Memory Info': // Legacy
  318. return event._meta?.contexts?.['Memory Info'] ?? defaultMeta;
  319. case 'threadpool_info': // Current
  320. case 'ThreadPool Info': // Legacy
  321. return event._meta?.contexts?.['ThreadPool Info'] ?? defaultMeta;
  322. case 'user':
  323. return event._meta?.user ?? defaultMeta;
  324. default:
  325. return defaultMeta;
  326. }
  327. }
  328. export function getContextIcon({
  329. type,
  330. value = {},
  331. }: {
  332. type: string;
  333. value?: Record<string, any>;
  334. }): React.ReactNode {
  335. let iconName = '';
  336. switch (type) {
  337. case 'device':
  338. iconName = generateIconName(value?.model);
  339. break;
  340. case 'client_os':
  341. case 'os':
  342. iconName = generateIconName(value?.name);
  343. break;
  344. case 'runtime':
  345. case 'browser':
  346. iconName = generateIconName(value?.name, value?.version);
  347. break;
  348. case 'user':
  349. const user = removeFilterMaskedEntries(value);
  350. return <UserAvatar user={user as AvatarUser} size={14} gravatar={false} />;
  351. case 'gpu':
  352. iconName = generateIconName(value?.vendor_name ? value?.vendor_name : value?.name);
  353. break;
  354. default:
  355. break;
  356. }
  357. if (iconName.length === 0) {
  358. return null;
  359. }
  360. return <ContextIcon name={iconName} size="sm" />;
  361. }
  362. export function getFormattedContextData({
  363. event,
  364. contextType,
  365. contextValue,
  366. organization,
  367. project,
  368. }: {
  369. contextType: string;
  370. contextValue: any;
  371. event: Event;
  372. organization: Organization;
  373. project?: Project;
  374. }): KeyValueListData {
  375. const meta = getContextMeta(event, contextType);
  376. switch (contextType) {
  377. case 'app':
  378. return [
  379. ...getKnownAppContextData({data: contextValue, event, meta}),
  380. ...getUnknownAppContextData({data: contextValue, meta}),
  381. ];
  382. case 'device':
  383. return [
  384. ...getKnownDeviceContextData({data: contextValue, event, meta}),
  385. ...getUnknownDeviceContextData({data: contextValue, meta}),
  386. ];
  387. case 'memory_info': // Current
  388. case 'Memory Info': // Legacy
  389. return [
  390. ...getKnownMemoryInfoContextData({data: contextValue, event, meta}),
  391. ...getUnknownMemoryInfoContextData({data: contextValue, meta}),
  392. ];
  393. case 'browser':
  394. return [
  395. ...getKnownBrowserContextData({data: contextValue, meta}),
  396. ...getUnknownBrowserContextData({data: contextValue, meta}),
  397. ];
  398. case 'os':
  399. return [
  400. ...getKnownOperatingSystemContextData({data: contextValue, meta}),
  401. ...getUnknownOperatingSystemContextData({data: contextValue, meta}),
  402. ];
  403. case 'unity':
  404. return [
  405. ...getKnownUnityContextData({data: contextValue, meta}),
  406. ...getUnknownUnityContextData({data: contextValue, meta}),
  407. ];
  408. case 'runtime':
  409. return [
  410. ...getKnownRuntimeContextData({data: contextValue, meta}),
  411. ...getUnknownRuntimeContextData({data: contextValue, meta}),
  412. ];
  413. case 'user':
  414. return [
  415. ...getKnownUserContextData({data: contextValue, meta}),
  416. ...getUnknownUserContextData({data: contextValue, meta}),
  417. ];
  418. case 'gpu':
  419. return [
  420. ...getKnownGpuContextData({data: contextValue, meta}),
  421. ...getUnknownGpuContextData({data: contextValue, meta}),
  422. ];
  423. case 'trace':
  424. return [
  425. ...getKnownTraceContextData({data: contextValue, event, meta, organization}),
  426. ...getUnknownTraceContextData({data: contextValue, meta}),
  427. ];
  428. case 'threadpool_info': // Current
  429. case 'ThreadPool Info': // Legacy
  430. return [
  431. ...getKnownThreadPoolInfoContextData({data: contextValue, event, meta}),
  432. ...getUnknownThreadPoolInfoContextData({data: contextValue, meta}),
  433. ];
  434. case 'redux.state':
  435. return getReduxContextData({data: contextValue});
  436. case 'state':
  437. return [
  438. ...getKnownStateContextData({data: contextValue, meta}),
  439. ...getUnknownStateContextData({data: contextValue, meta}),
  440. ];
  441. case 'profile':
  442. return [
  443. ...getKnownProfileContextData({data: contextValue, meta, organization, project}),
  444. ...getUnknownProfileContextData({data: contextValue, meta}),
  445. ];
  446. case 'replay':
  447. return [
  448. ...getKnownReplayContextData({data: contextValue, meta, organization}),
  449. ...getUnknownReplayContextData({data: contextValue, meta}),
  450. ];
  451. default:
  452. return getDefaultContextData(contextValue);
  453. }
  454. }
  455. const RelativeTime = styled('span')`
  456. color: ${p => p.theme.subText};
  457. margin-left: ${space(0.5)};
  458. `;
  459. export const CONTEXT_DOCS_LINK = `https://docs.sentry.io/platform-redirect/?next=/enriching-events/context/`;