spanEvidenceKeyValueList.tsx 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723
  1. import type {ReactNode} from 'react';
  2. import {Fragment, useMemo} from 'react';
  3. import styled from '@emotion/styled';
  4. import type {Location} from 'history';
  5. import kebabCase from 'lodash/kebabCase';
  6. import mapValues from 'lodash/mapValues';
  7. import {Button} from 'sentry/components/button';
  8. import ClippedBox from 'sentry/components/clippedBox';
  9. import {CodeSnippet} from 'sentry/components/codeSnippet';
  10. import {getKeyValueListData as getRegressionIssueKeyValueList} from 'sentry/components/events/eventStatisticalDetector/eventRegressionSummary';
  11. import {getSpanInfoFromTransactionEvent} from 'sentry/components/events/interfaces/performance/utils';
  12. import {AnnotatedText} from 'sentry/components/events/meta/annotatedText';
  13. import Link from 'sentry/components/links/link';
  14. import {t} from 'sentry/locale';
  15. import type {
  16. Entry,
  17. EntryRequest,
  18. Event,
  19. EventTransaction,
  20. KeyValueListData,
  21. KeyValueListDataItem,
  22. Organization,
  23. } from 'sentry/types';
  24. import {
  25. EntryType,
  26. getIssueTypeFromOccurrenceType,
  27. isOccurrenceBased,
  28. IssueType,
  29. isTransactionBased,
  30. } from 'sentry/types';
  31. import {formatBytesBase2} from 'sentry/utils';
  32. import {generateLinkToEventInTraceView} from 'sentry/utils/discover/urls';
  33. import toRoundedPercent from 'sentry/utils/number/toRoundedPercent';
  34. import {safeURL} from 'sentry/utils/url/safeURL';
  35. import {useLocation} from 'sentry/utils/useLocation';
  36. import useOrganization from 'sentry/utils/useOrganization';
  37. import {transactionSummaryRouteWithQuery} from 'sentry/views/performance/transactionSummary/utils';
  38. import {getPerformanceDuration} from 'sentry/views/performance/utils/getPerformanceDuration';
  39. import {SQLishFormatter} from 'sentry/views/starfish/utils/sqlish/SQLishFormatter';
  40. import KeyValueList from '../keyValueList';
  41. import type {ProcessedSpanType, RawSpanType} from '../spans/types';
  42. import {getSpanSubTimings, SpanSubTimingName} from '../spans/utils';
  43. import type {TraceContextSpanProxy} from './spanEvidence';
  44. const formatter = new SQLishFormatter();
  45. type Span = (RawSpanType | TraceContextSpanProxy) & {
  46. data?: any;
  47. start_timestamp?: number;
  48. timestamp?: number;
  49. };
  50. type SpanEvidenceKeyValueListProps = {
  51. causeSpans: Span[];
  52. event: EventTransaction;
  53. location: Location;
  54. offendingSpans: Span[];
  55. organization: Organization;
  56. parentSpan: Span | null;
  57. issueType?: IssueType;
  58. projectSlug?: string;
  59. };
  60. const TEST_ID_NAMESPACE = 'span-evidence-key-value-list';
  61. function ConsecutiveDBQueriesSpanEvidence({
  62. event,
  63. causeSpans,
  64. offendingSpans,
  65. organization,
  66. projectSlug,
  67. location,
  68. }: SpanEvidenceKeyValueListProps) {
  69. return (
  70. <PresortedKeyValueList
  71. data={
  72. [
  73. makeTransactionNameRow(event, organization, location, projectSlug),
  74. causeSpans
  75. ? makeRow(t('Starting Span'), getSpanEvidenceValue(causeSpans[0]))
  76. : null,
  77. makeRow('Parallelizable Spans', offendingSpans.map(getSpanEvidenceValue)),
  78. makeRow(
  79. t('Duration Impact'),
  80. getDurationImpact(
  81. event,
  82. getConsecutiveDbTimeSaved(causeSpans, offendingSpans)
  83. )
  84. ),
  85. ].filter(Boolean) as KeyValueListData
  86. }
  87. />
  88. );
  89. }
  90. function ConsecutiveHTTPSpanEvidence({
  91. event,
  92. offendingSpans,
  93. organization,
  94. projectSlug,
  95. location,
  96. }: SpanEvidenceKeyValueListProps) {
  97. return (
  98. <PresortedKeyValueList
  99. data={
  100. [
  101. makeTransactionNameRow(event, organization, location, projectSlug),
  102. makeRow(
  103. 'Offending Spans',
  104. offendingSpans.map(span => span.description)
  105. ),
  106. ].filter(Boolean) as KeyValueListData
  107. }
  108. />
  109. );
  110. }
  111. function LargeHTTPPayloadSpanEvidence({
  112. event,
  113. offendingSpans,
  114. organization,
  115. projectSlug,
  116. location,
  117. }: SpanEvidenceKeyValueListProps) {
  118. return (
  119. <PresortedKeyValueList
  120. data={
  121. [
  122. makeTransactionNameRow(event, organization, location, projectSlug),
  123. makeRow(t('Large HTTP Payload Span'), getSpanEvidenceValue(offendingSpans[0])),
  124. makeRow(
  125. t('Payload Size'),
  126. getSpanFieldBytes(offendingSpans[0], 'http.response_content_length') ??
  127. getSpanFieldBytes(offendingSpans[0], 'Encoded Body Size')
  128. ),
  129. ].filter(Boolean) as KeyValueListData
  130. }
  131. />
  132. );
  133. }
  134. function HTTPOverheadSpanEvidence({
  135. event,
  136. offendingSpans,
  137. organization,
  138. projectSlug,
  139. location,
  140. }: SpanEvidenceKeyValueListProps) {
  141. return (
  142. <PresortedKeyValueList
  143. data={
  144. [
  145. makeTransactionNameRow(event, organization, location, projectSlug),
  146. makeRow(t('Max Queue Time'), getHTTPOverheadMaxTime(offendingSpans)),
  147. ].filter(Boolean) as KeyValueListData
  148. }
  149. />
  150. );
  151. }
  152. function NPlusOneDBQueriesSpanEvidence({
  153. event,
  154. causeSpans,
  155. parentSpan,
  156. offendingSpans,
  157. organization,
  158. projectSlug,
  159. location,
  160. }: SpanEvidenceKeyValueListProps) {
  161. const dbSpans = offendingSpans.filter(span => (span.op || '').startsWith('db'));
  162. const repeatingSpanRows = dbSpans
  163. .filter(span => offendingSpans.find(s => s.hash === span.hash) === span)
  164. .map((span, i) =>
  165. makeRow(
  166. i === 0 ? t('Repeating Spans (%s)', dbSpans.length) : '',
  167. getSpanEvidenceValue(span)
  168. )
  169. );
  170. return (
  171. <PresortedKeyValueList
  172. data={
  173. [
  174. makeTransactionNameRow(event, organization, location, projectSlug),
  175. parentSpan ? makeRow(t('Parent Span'), getSpanEvidenceValue(parentSpan)) : null,
  176. causeSpans.length > 0
  177. ? makeRow(t('Preceding Span'), getSpanEvidenceValue(causeSpans[0]))
  178. : null,
  179. ...repeatingSpanRows,
  180. ].filter(Boolean) as KeyValueListData
  181. }
  182. />
  183. );
  184. }
  185. function NPlusOneAPICallsSpanEvidence({
  186. event,
  187. offendingSpans,
  188. organization,
  189. projectSlug,
  190. location,
  191. }: SpanEvidenceKeyValueListProps) {
  192. const requestEntry = event?.entries?.find(isRequestEntry);
  193. const baseURL = requestEntry?.data?.url;
  194. const problemParameters = formatChangingQueryParameters(offendingSpans, baseURL);
  195. const commonPathPrefix = formatBasePath(offendingSpans[0], baseURL);
  196. return (
  197. <PresortedKeyValueList
  198. data={
  199. [
  200. makeTransactionNameRow(event, organization, location, projectSlug),
  201. commonPathPrefix
  202. ? makeRow(
  203. t('Repeating Spans (%s)', offendingSpans.length),
  204. <pre className="val-string">
  205. <AnnotatedText
  206. value={
  207. <Fragment>
  208. {commonPathPrefix}
  209. <HighlightedEvidence>[Parameters]</HighlightedEvidence>
  210. </Fragment>
  211. }
  212. />
  213. </pre>
  214. )
  215. : null,
  216. problemParameters.length > 0
  217. ? makeRow(t('Parameters'), problemParameters)
  218. : null,
  219. ].filter(Boolean) as KeyValueListData
  220. }
  221. />
  222. );
  223. }
  224. function MainThreadFunctionEvidence({
  225. event,
  226. organization,
  227. }: SpanEvidenceKeyValueListProps) {
  228. const data = useMemo(() => {
  229. const dataRows: KeyValueListDataItem[] = [];
  230. const evidenceData = event.occurrence?.evidenceData ?? {};
  231. const evidenceDisplay = event.occurrence?.evidenceDisplay ?? [];
  232. if (evidenceData.transactionName) {
  233. const transactionSummaryLocation = transactionSummaryRouteWithQuery({
  234. orgSlug: organization.slug,
  235. projectID: event.projectID,
  236. transaction: evidenceData.transactionName,
  237. query: {},
  238. });
  239. dataRows.push(
  240. makeRow(
  241. t('Transaction'),
  242. <pre>
  243. <Link to={transactionSummaryLocation}>{evidenceData.transactionName}</Link>
  244. </pre>
  245. )
  246. );
  247. }
  248. dataRows.push(
  249. ...evidenceDisplay.map(item => ({
  250. subject: item.name,
  251. key: item.name,
  252. value: item.value,
  253. }))
  254. );
  255. return dataRows;
  256. }, [event, organization]);
  257. return <PresortedKeyValueList data={data} />;
  258. }
  259. function RegressionEvidence({event, issueType}: SpanEvidenceKeyValueListProps) {
  260. const organization = useOrganization();
  261. const data = useMemo(
  262. () =>
  263. issueType ? getRegressionIssueKeyValueList(organization, issueType, event) : null,
  264. [organization, event, issueType]
  265. );
  266. return data ? <PresortedKeyValueList data={data} /> : null;
  267. }
  268. const PREVIEW_COMPONENTS = {
  269. [IssueType.PERFORMANCE_N_PLUS_ONE_DB_QUERIES]: NPlusOneDBQueriesSpanEvidence,
  270. [IssueType.PERFORMANCE_N_PLUS_ONE_API_CALLS]: NPlusOneAPICallsSpanEvidence,
  271. [IssueType.PERFORMANCE_SLOW_DB_QUERY]: SlowDBQueryEvidence,
  272. [IssueType.PERFORMANCE_CONSECUTIVE_DB_QUERIES]: ConsecutiveDBQueriesSpanEvidence,
  273. [IssueType.PERFORMANCE_RENDER_BLOCKING_ASSET]: RenderBlockingAssetSpanEvidence,
  274. [IssueType.PERFORMANCE_UNCOMPRESSED_ASSET]: UncompressedAssetSpanEvidence,
  275. [IssueType.PERFORMANCE_CONSECUTIVE_HTTP]: ConsecutiveHTTPSpanEvidence,
  276. [IssueType.PERFORMANCE_LARGE_HTTP_PAYLOAD]: LargeHTTPPayloadSpanEvidence,
  277. [IssueType.PERFORMANCE_HTTP_OVERHEAD]: HTTPOverheadSpanEvidence,
  278. [IssueType.PERFORMANCE_DURATION_REGRESSION]: RegressionEvidence,
  279. [IssueType.PERFORMANCE_ENDPOINT_REGRESSION]: RegressionEvidence,
  280. [IssueType.PROFILE_FILE_IO_MAIN_THREAD]: MainThreadFunctionEvidence,
  281. [IssueType.PROFILE_IMAGE_DECODE_MAIN_THREAD]: MainThreadFunctionEvidence,
  282. [IssueType.PROFILE_JSON_DECODE_MAIN_THREAD]: MainThreadFunctionEvidence,
  283. [IssueType.PROFILE_REGEX_MAIN_THREAD]: MainThreadFunctionEvidence,
  284. [IssueType.PROFILE_FRAME_DROP]: MainThreadFunctionEvidence,
  285. [IssueType.PROFILE_FRAME_DROP_EXPERIMENTAL]: MainThreadFunctionEvidence,
  286. [IssueType.PROFILE_FUNCTION_REGRESSION]: RegressionEvidence,
  287. [IssueType.PROFILE_FUNCTION_REGRESSION_EXPERIMENTAL]: RegressionEvidence,
  288. };
  289. export function SpanEvidenceKeyValueList({
  290. event,
  291. projectSlug,
  292. }: {
  293. event: EventTransaction;
  294. projectSlug?: string;
  295. }) {
  296. const organization = useOrganization();
  297. const location = useLocation();
  298. const spanInfo = getSpanInfoFromTransactionEvent(event);
  299. const typeId = event.occurrence?.type;
  300. const issueType =
  301. event.perfProblem?.issueType ?? getIssueTypeFromOccurrenceType(typeId);
  302. const requiresSpanInfo = isTransactionBased(typeId) && isOccurrenceBased(typeId);
  303. if (!issueType || (requiresSpanInfo && !spanInfo)) {
  304. return (
  305. <DefaultSpanEvidence
  306. event={event}
  307. offendingSpans={[]}
  308. location={location}
  309. causeSpans={[]}
  310. parentSpan={null}
  311. organization={organization}
  312. projectSlug={projectSlug}
  313. />
  314. );
  315. }
  316. const Component = PREVIEW_COMPONENTS[issueType] ?? DefaultSpanEvidence;
  317. return (
  318. <ClippedBox clipHeight={300}>
  319. <Component
  320. event={event}
  321. issueType={issueType}
  322. organization={organization}
  323. location={location}
  324. projectSlug={projectSlug}
  325. {...spanInfo}
  326. />
  327. </ClippedBox>
  328. );
  329. }
  330. const HighlightedEvidence = styled('span')`
  331. color: ${p => p.theme.errorText};
  332. `;
  333. const isRequestEntry = (entry: Entry): entry is EntryRequest => {
  334. return entry.type === EntryType.REQUEST;
  335. };
  336. function SlowDBQueryEvidence({
  337. event,
  338. offendingSpans,
  339. organization,
  340. projectSlug,
  341. location,
  342. }: SpanEvidenceKeyValueListProps) {
  343. return (
  344. <PresortedKeyValueList
  345. data={[
  346. makeTransactionNameRow(event, organization, location, projectSlug),
  347. makeRow(t('Slow DB Query'), getSpanEvidenceValue(offendingSpans[0])),
  348. makeRow(
  349. t('Duration Impact'),
  350. getSingleSpanDurationImpact(event, offendingSpans[0])
  351. ),
  352. ]}
  353. />
  354. );
  355. }
  356. function RenderBlockingAssetSpanEvidence({
  357. event,
  358. offendingSpans,
  359. organization,
  360. projectSlug,
  361. location,
  362. }: SpanEvidenceKeyValueListProps) {
  363. const offendingSpan = offendingSpans[0]; // For render-blocking assets, there is only one offender
  364. return (
  365. <PresortedKeyValueList
  366. data={[
  367. makeTransactionNameRow(event, organization, location, projectSlug),
  368. makeRow(t('Slow Resource Span'), getSpanEvidenceValue(offendingSpan)),
  369. makeRow(
  370. t('FCP Delay'),
  371. formatDelay(getSpanDuration(offendingSpan), event.measurements?.fcp?.value ?? 0)
  372. ),
  373. makeRow(t('Duration Impact'), getSingleSpanDurationImpact(event, offendingSpan)),
  374. ]}
  375. />
  376. );
  377. }
  378. function UncompressedAssetSpanEvidence({
  379. event,
  380. offendingSpans,
  381. organization,
  382. projectSlug,
  383. location,
  384. }: SpanEvidenceKeyValueListProps) {
  385. return (
  386. <PresortedKeyValueList
  387. data={[
  388. makeTransactionNameRow(event, organization, location, projectSlug),
  389. makeRow(t('Slow Resource Span'), getSpanEvidenceValue(offendingSpans[0])),
  390. makeRow(
  391. t('Asset Size'),
  392. getSpanFieldBytes(offendingSpans[0], 'http.response_content_length') ??
  393. getSpanFieldBytes(offendingSpans[0], 'Encoded Body Size')
  394. ),
  395. makeRow(
  396. t('Duration Impact'),
  397. getSingleSpanDurationImpact(event, offendingSpans[0])
  398. ),
  399. ]}
  400. />
  401. );
  402. }
  403. function DefaultSpanEvidence({
  404. event,
  405. offendingSpans,
  406. organization,
  407. projectSlug,
  408. location,
  409. }: SpanEvidenceKeyValueListProps) {
  410. return (
  411. <PresortedKeyValueList
  412. data={
  413. [
  414. makeTransactionNameRow(event, organization, location, projectSlug),
  415. offendingSpans.length > 0
  416. ? makeRow(t('Offending Span'), getSpanEvidenceValue(offendingSpans[0]))
  417. : null,
  418. ].filter(Boolean) as KeyValueListData
  419. }
  420. />
  421. );
  422. }
  423. function PresortedKeyValueList({data}: {data: KeyValueListData}) {
  424. return <KeyValueList shouldSort={false} data={data} />;
  425. }
  426. const makeTransactionNameRow = (
  427. event: Event,
  428. organization: Organization,
  429. location: Location,
  430. projectSlug?: string
  431. ) => {
  432. const transactionSummaryLocation = transactionSummaryRouteWithQuery({
  433. orgSlug: organization.slug,
  434. projectID: event.projectID,
  435. transaction: event.title,
  436. query: {},
  437. });
  438. const traceSlug = event.contexts?.trace?.trace_id ?? '';
  439. const eventDetailsLocation = generateLinkToEventInTraceView({
  440. traceSlug,
  441. projectSlug: projectSlug ?? '',
  442. eventId: event.eventID,
  443. timestamp: event.endTimestamp ?? '',
  444. location,
  445. organization,
  446. });
  447. const actionButton = projectSlug ? (
  448. <Button size="xs" to={eventDetailsLocation}>
  449. {t('View Full Event')}
  450. </Button>
  451. ) : undefined;
  452. return makeRow(
  453. t('Transaction'),
  454. <pre>
  455. <Link to={transactionSummaryLocation}>{event.title}</Link>
  456. </pre>,
  457. actionButton
  458. );
  459. };
  460. const makeRow = (
  461. subject: KeyValueListDataItem['subject'],
  462. value: KeyValueListDataItem['value'],
  463. actionButton?: ReactNode
  464. ): KeyValueListDataItem => {
  465. const itemKey = kebabCase(subject);
  466. return {
  467. key: itemKey,
  468. subject,
  469. value,
  470. subjectDataTestId: `${TEST_ID_NAMESPACE}.${itemKey}`,
  471. isMultiValue: Array.isArray(value),
  472. actionButton,
  473. };
  474. };
  475. function getSpanEvidenceValue(span: Span | null) {
  476. if (!span || (!span.op && !span.description)) {
  477. return t('(no value)');
  478. }
  479. if (!span.op && span.description) {
  480. return span.description;
  481. }
  482. if (span.op && !span.description) {
  483. return span.op;
  484. }
  485. if (span.op === 'db' && span.description) {
  486. return (
  487. <StyledCodeSnippet language="sql">
  488. {formatter.toString(span.description)}
  489. </StyledCodeSnippet>
  490. );
  491. }
  492. return `${span.op} - ${span.description}`;
  493. }
  494. const StyledCodeSnippet = styled(CodeSnippet)`
  495. pre {
  496. /* overflow is set to visible in global styles so need to enforce auto here */
  497. overflow: auto !important;
  498. }
  499. z-index: 0;
  500. `;
  501. const getConsecutiveDbTimeSaved = (
  502. consecutiveSpans: Span[],
  503. independentSpans: Span[]
  504. ): number => {
  505. const totalDuration = sumSpanDurations(consecutiveSpans);
  506. const maxIndependentSpanDuration = Math.max(
  507. ...independentSpans.map(span => getSpanDuration(span))
  508. );
  509. const independentSpanIds = independentSpans.map(span => span.span_id);
  510. let sumOfDependentSpansDuration = 0;
  511. consecutiveSpans.forEach(span => {
  512. if (!independentSpanIds.includes(span.span_id)) {
  513. sumOfDependentSpansDuration += getSpanDuration(span);
  514. }
  515. });
  516. return (
  517. totalDuration - Math.max(maxIndependentSpanDuration, sumOfDependentSpansDuration)
  518. );
  519. };
  520. const getHTTPOverheadMaxTime = (offendingSpans: Span[]): string | null => {
  521. const slowestSpanTimings = getSpanSubTimings(
  522. offendingSpans[offendingSpans.length - 1] as ProcessedSpanType
  523. );
  524. if (!slowestSpanTimings) {
  525. return null;
  526. }
  527. const waitTimeTiming = slowestSpanTimings.find(
  528. timing => timing.name === SpanSubTimingName.WAIT_TIME
  529. );
  530. if (!waitTimeTiming) {
  531. return null;
  532. }
  533. return getPerformanceDuration(waitTimeTiming.duration * 1000);
  534. };
  535. const sumSpanDurations = (spans: Span[]) => {
  536. let totalDuration = 0;
  537. spans.forEach(span => {
  538. totalDuration += getSpanDuration(span);
  539. });
  540. return totalDuration;
  541. };
  542. const getSpanDuration = ({timestamp, start_timestamp}: Span) => {
  543. return ((timestamp ?? 0) - (start_timestamp ?? 0)) * 1000;
  544. };
  545. function getDurationImpact(event: EventTransaction, durationAdded: number) {
  546. const transactionTime = (event.endTimestamp - event.startTimestamp) * 1000;
  547. if (!transactionTime) {
  548. return null;
  549. }
  550. return formatDurationImpact(durationAdded, transactionTime);
  551. }
  552. function formatDurationImpact(durationAdded: number, totalDuration: number) {
  553. const percent = durationAdded / totalDuration;
  554. return `${toRoundedPercent(percent)} (${getPerformanceDuration(
  555. durationAdded
  556. )}/${getPerformanceDuration(totalDuration)})`;
  557. }
  558. function formatDelay(durationAdded: number, totalDuration: number) {
  559. const percent = durationAdded / totalDuration;
  560. return `${getPerformanceDuration(durationAdded)} (${toRoundedPercent(
  561. percent
  562. )} of ${getPerformanceDuration(totalDuration)})`;
  563. }
  564. function getSingleSpanDurationImpact(event: EventTransaction, span: Span) {
  565. return getDurationImpact(event, getSpanDuration(span));
  566. }
  567. function getSpanDataField(span: Span, field: string) {
  568. return span.data?.[field];
  569. }
  570. function getSpanFieldBytes(span: Span, field: string) {
  571. const bytes = getSpanDataField(span, field);
  572. if (!bytes) {
  573. return null;
  574. }
  575. return `${formatBytesBase2(bytes)} (${bytes} B)`;
  576. }
  577. type ParameterLookup = Record<string, string[]>;
  578. /**
  579. * Extracts changing URL query parameters from a list of `http.client` spans.
  580. * e.g.,
  581. *
  582. * https://service.io/r?id=1&filter=none
  583. * https://service.io/r?id=2&filter=none
  584. * https://service.io/r?id=3&filter=none
  585. *
  586. * @returns A condensed string describing the query parameters changing
  587. * between the URLs of the given span. e.g., "id:{1,2,3}"
  588. */
  589. function formatChangingQueryParameters(spans: Span[], baseURL?: string): string[] {
  590. const URLs = spans
  591. .map(span => extractSpanURLString(span, baseURL))
  592. .filter((url): url is URL => url instanceof URL);
  593. const allQueryParameters = extractQueryParameters(URLs);
  594. const pairs: string[] = [];
  595. for (const key in allQueryParameters) {
  596. const values = allQueryParameters[key];
  597. // By definition, if the parameter only has one value that means it's not
  598. // changing between calls, so omit it!
  599. if (values.length > 1) {
  600. pairs.push(`${key}:{${values.join(',')}}`);
  601. }
  602. }
  603. return pairs;
  604. }
  605. /**
  606. * Parses the span data and pulls out the URL. Accounts for different SDKs and
  607. * different versions of SDKs formatting and parsing the URL contents
  608. * differently. Mirror of `get_url_from_span`. Ideally, this should not exist,
  609. * and instead it should use the data provided by the backend
  610. */
  611. export const extractSpanURLString = (span: Span, baseURL?: string): URL | null => {
  612. let url = span?.data?.url;
  613. if (url) {
  614. const query = span.data['http.query'];
  615. if (query) {
  616. url += `?${query}`;
  617. }
  618. const parsedURL = safeURL(url, baseURL);
  619. if (parsedURL) {
  620. return parsedURL;
  621. }
  622. }
  623. const [_method, _url] = (span?.description ?? '').split(' ', 2);
  624. return safeURL(_url, baseURL) ?? null;
  625. };
  626. export function extractQueryParameters(URLs: URL[]): ParameterLookup {
  627. const parameterValuesByKey: ParameterLookup = {};
  628. URLs.forEach(url => {
  629. for (const [key, value] of url.searchParams) {
  630. parameterValuesByKey[key] ??= [];
  631. parameterValuesByKey[key].push(value);
  632. }
  633. });
  634. return mapValues(parameterValuesByKey, parameterList => {
  635. return Array.from(new Set(parameterList));
  636. });
  637. }
  638. function formatBasePath(span: Span, baseURL?: string): string {
  639. const spanURL = extractSpanURLString(span, baseURL);
  640. return spanURL?.pathname ?? '';
  641. }