1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537 |
- import {OrganizationFixture} from 'sentry-fixture/organization';
- import {EntryType} from 'sentry/types/event';
- import type {SiblingAutogroupNode} from 'sentry/views/performance/newTraceDetails/traceModels/siblingAutogroupNode';
- import {DEFAULT_TRACE_VIEW_PREFERENCES} from 'sentry/views/performance/newTraceDetails/traceState/tracePreferences';
- import type {ReplayRecord} from 'sentry/views/replays/types';
- import {
- isMissingInstrumentationNode,
- isParentAutogroupedNode,
- isSiblingAutogroupedNode,
- isSpanNode,
- isTransactionNode,
- } from './../traceGuards';
- import type {ParentAutogroupNode} from './parentAutogroupNode';
- import {TraceTree} from './traceTree';
- import {
- assertTransactionNode,
- makeEventTransaction,
- makeSpan,
- makeTrace,
- makeTraceError,
- makeTracePerformanceIssue,
- makeTransaction,
- } from './traceTreeTestUtils';
- function mockSpansResponse(
- spans: TraceTree.Span[],
- project_slug: string,
- event_id: string
- ): jest.Mock<any, any> {
- return MockApiClient.addMockResponse({
- url: `/organizations/org-slug/events/${project_slug}:${event_id}/?averageColumn=span.self_time&averageColumn=span.duration`,
- method: 'GET',
- body: makeEventTransaction({
- entries: [{type: EntryType.SPANS, data: spans}],
- }),
- });
- }
- const start = new Date('2024-02-29T00:00:00Z').getTime() / 1e3;
- const end = new Date('2024-02-29T00:00:00Z').getTime() / 1e3 + 5;
- const traceMetadata = {replay: null, meta: null};
- const trace = makeTrace({
- transactions: [
- makeTransaction({
- start_timestamp: start,
- timestamp: start + 2,
- children: [makeTransaction({start_timestamp: start + 1, timestamp: start + 4})],
- }),
- ],
- orphan_errors: [],
- });
- const traceWithEventId = makeTrace({
- transactions: [
- makeTransaction({
- event_id: 'event-id',
- start_timestamp: start,
- timestamp: start + 2,
- project_slug: 'project',
- children: [
- makeTransaction({
- start_timestamp: start + 1,
- timestamp: start + 4,
- event_id: 'child-event-id',
- project_slug: 'project',
- }),
- ],
- }),
- ],
- });
- const traceWithVitals = makeTrace({
- transactions: [
- makeTransaction({
- start_timestamp: start,
- timestamp: start + 2,
- measurements: {ttfb: {value: 0, unit: 'millisecond'}},
- }),
- ],
- });
- const traceWithOrphanError = makeTrace({
- transactions: [
- makeTransaction({
- start_timestamp: start,
- timestamp: start + 2,
- children: [makeTransaction({start_timestamp: start + 1, timestamp: start + 2})],
- }),
- ],
- orphan_errors: [makeTraceError({level: 'error', timestamp: end})],
- });
- const outOfOrderTrace = makeTrace({
- transactions: [
- makeTransaction({
- start_timestamp: 1,
- transaction: 'last',
- children: [],
- }),
- makeTransaction({start_timestamp: 0, transaction: 'first'}),
- ],
- });
- const siblingAutogroupSpans = [
- makeSpan({
- op: 'db',
- description: 'redis',
- start_timestamp: start,
- timestamp: start + 1,
- }),
- makeSpan({
- op: 'db',
- description: 'redis',
- start_timestamp: start,
- timestamp: start + 1,
- }),
- makeSpan({
- op: 'db',
- description: 'redis',
- start_timestamp: start,
- timestamp: start + 1,
- }),
- makeSpan({
- op: 'db',
- description: 'redis',
- start_timestamp: start,
- timestamp: start + 1,
- }),
- makeSpan({
- op: 'db',
- description: 'redis',
- start_timestamp: start,
- timestamp: start + 1,
- }),
- ];
- const parentAutogroupSpans = [
- makeSpan({op: 'db', description: 'redis', span_id: '0000'}),
- makeSpan({op: 'db', description: 'redis', span_id: '0001', parent_span_id: '0000'}),
- ];
- const parentAutogroupSpansWithTailChildren = [
- makeSpan({op: 'db', description: 'redis', span_id: '0000'}),
- makeSpan({
- op: 'db',
- description: 'redis',
- span_id: '0001',
- parent_span_id: '0000',
- }),
- makeSpan({
- op: 'http',
- description: 'request',
- span_id: '0002',
- parent_span_id: '0001',
- }),
- ];
- function findTransactionByEventId(tree: TraceTree, eventId: string) {
- return TraceTree.Find(
- tree.root,
- node => isTransactionNode(node) && node.value.event_id === eventId
- );
- }
- describe('TraceTree', () => {
- describe('aggreagate node properties', () => {
- it('adds errors to node', () => {
- const tree = TraceTree.FromTrace(
- makeTrace({
- transactions: [
- makeTransaction({
- errors: [makeTraceError()],
- }),
- ],
- }),
- traceMetadata
- );
- expect(tree.root.children[0]!.errors.size).toBe(1);
- });
- it('stores trace error as error on node', () => {
- const tree = TraceTree.FromTrace(
- makeTrace({
- orphan_errors: [makeTraceError()],
- }),
- traceMetadata
- );
- expect(tree.root.children[0]!.children[0]!.errors.size).toBe(1);
- });
- it('adds performance issues to node', () => {
- const tree = TraceTree.FromTrace(
- makeTrace({
- transactions: [
- makeTransaction({
- performance_issues: [makeTracePerformanceIssue()],
- }),
- ],
- }),
- traceMetadata
- );
- expect(tree.root.children[0]!.children[0]!.performance_issues.size).toBe(1);
- });
- it('adds transaction profile to node', () => {
- const tree = TraceTree.FromTrace(
- makeTrace({
- transactions: [
- makeTransaction({
- profile_id: 'profile-id',
- }),
- ],
- }),
- traceMetadata
- );
- expect(tree.root.children[0]!.children[0]!.profiles).toHaveLength(1);
- });
- it('adds continuous profile to node', () => {
- const tree = TraceTree.FromTrace(
- makeTrace({
- transactions: [
- makeTransaction({
- profiler_id: 'profile-id',
- children: [],
- }),
- ],
- }),
- traceMetadata
- );
- expect(tree.root.children[0]!.children[0]!.profiles).toHaveLength(1);
- });
- });
- describe('adjusts trace start and end', () => {
- it('based off min(events.start_timestamp)', () => {
- const tree = TraceTree.FromTrace(trace, traceMetadata);
- expect(tree.root.space[0]).toBe(trace.transactions[0]!.start_timestamp * 1e3);
- });
- it('based off max(events.timestamp)', () => {
- const tree = TraceTree.FromTrace(trace, traceMetadata);
- expect(tree.root.space[1]).toBe(4000);
- });
- // This happnes for errors only traces
- it('end,0 when we cannot construct a timeline', () => {
- const tree = TraceTree.FromTrace(
- makeTrace({orphan_errors: [makeTraceError({level: 'error', timestamp: end})]}),
- traceMetadata
- );
- expect(tree.root.space[0]).toBe(end * 1e3);
- expect(tree.root.space[1]).toBe(0);
- });
- it('considers all children when inferring start and end', () => {
- const tree = TraceTree.FromTrace(
- makeTrace({
- transactions: [
- makeTransaction({
- start_timestamp: start,
- timestamp: start + 1,
- children: [],
- }),
- makeTransaction({
- start_timestamp: start - 1,
- timestamp: start + 2,
- children: [],
- }),
- ],
- orphan_errors: [],
- }),
- traceMetadata
- );
- expect(tree.root.space[1]).toBe(3000);
- expect(tree.root.space[0]).toBe(start * 1e3 - 1e3);
- });
- it('considers orphan errors when inferring end', () => {
- const tree = TraceTree.FromTrace(
- makeTrace({
- transactions: [
- makeTransaction({
- start_timestamp: start,
- timestamp: start + 1,
- children: [],
- }),
- ],
- orphan_errors: [
- makeTraceError({
- level: 'error',
- timestamp: start + 5,
- }),
- ],
- }),
- traceMetadata
- );
- expect(tree.root.space[1]).toBe(5000);
- expect(tree.root.space[0]).toBe(start * 1e3);
- });
- it('replay record extends trace start', () => {
- const replayStart = new Date('2024-02-29T00:00:00Z').getTime();
- const replayEnd = new Date(replayStart + 5000).getTime();
- const tree = TraceTree.FromTrace(
- makeTrace({
- transactions: [
- makeTransaction({
- start_timestamp: replayStart / 1e3 + 0.1,
- timestamp: replayStart / 1e3 + 0.1,
- }),
- ],
- }),
- {
- meta: null,
- replay: {
- started_at: new Date(replayStart),
- finished_at: new Date(replayEnd),
- } as ReplayRecord,
- }
- );
- expect(tree.root.space[0]).toBe(replayStart);
- expect(tree.root.space[1]).toBe(5000);
- });
- it('measurements extend trace start and end', () => {
- const tree = TraceTree.FromTrace(
- makeTrace({
- transactions: [
- makeTransaction({
- start_timestamp: start,
- timestamp: start + 1,
- children: [],
- measurements: {
- ttfb: {
- unit: 'millisecond',
- value: -5000,
- },
- lcp: {
- unit: 'millisecond',
- value: 5000,
- },
- },
- }),
- ],
- orphan_errors: [],
- }),
- traceMetadata
- );
- expect(tree.root.space).toEqual([start * 1e3 - 5000, 10_000]);
- });
- });
- describe('indicators', () => {
- it('measurements are converted to indicators', () => {
- const tree = TraceTree.FromTrace(traceWithVitals, traceMetadata);
- expect(tree.indicators).toHaveLength(1);
- expect(tree.indicators[0]!.start).toBe(start * 1e3);
- });
- it('sorts indicators by start', () => {
- const tree = TraceTree.FromTrace(
- makeTrace({
- transactions: [
- makeTransaction({
- start_timestamp: 0,
- timestamp: 1,
- measurements: {
- ttfb: {value: 2000, unit: 'millisecond'},
- lcp: {value: 1000, unit: 'millisecond'},
- },
- }),
- ],
- }),
- traceMetadata
- );
- expect(tree.indicators).toHaveLength(2);
- expect(tree.indicators[0]!.start < tree.indicators[1]!.start).toBe(true);
- });
- });
- describe('FromTrace', () => {
- it('assembles tree from trace', () => {
- const tree = TraceTree.FromTrace(trace, traceMetadata);
- expect(tree.build().serialize()).toMatchSnapshot();
- });
- it('sorts by start_timestamp', () => {
- const tree = TraceTree.FromTrace(outOfOrderTrace, traceMetadata);
- expect(tree.build().serialize()).toMatchSnapshot();
- });
- it('inserts orphan error', () => {
- const tree = TraceTree.FromTrace(traceWithOrphanError, {
- meta: null,
- replay: null,
- });
- expect(tree.build().serialize()).toMatchSnapshot();
- });
- it('if parent span does not exist in span tree, the transaction stays under its previous parent', () => {
- const tree = TraceTree.FromTrace(
- makeTrace({
- transactions: [
- makeTransaction({
- transaction: 'root',
- children: [
- makeTransaction({transaction: 'child', parent_span_id: 'does not exist'}),
- ],
- }),
- ],
- }),
- traceMetadata
- );
- TraceTree.FromSpans(tree.root.children[0]!, [makeSpan()], makeEventTransaction());
- expect(tree.build().serialize()).toMatchSnapshot();
- });
- it('initializes canFetch based on spanChildrenCount', () => {
- const tree = TraceTree.FromTrace(
- makeTrace({
- transactions: [
- makeTransaction({
- event_id: 'transaction',
- children: [],
- }),
- makeTransaction({event_id: 'no-span-count-transaction'}),
- makeTransaction({event_id: 'no-spans-transaction', children: []}),
- ],
- }),
- {
- meta: {
- transactiontoSpanChildrenCount: {
- transaction: 10,
- 'no-spans-transaction': 1,
- // we have no data for child transaction
- },
- errors: 0,
- performance_issues: 0,
- projects: 0,
- transactions: 0,
- },
- replay: null,
- }
- );
- expect(findTransactionByEventId(tree, 'transaction')?.canFetch).toBe(true);
- expect(findTransactionByEventId(tree, 'no-span-count-transaction')?.canFetch).toBe(
- true
- );
- expect(findTransactionByEventId(tree, 'no-spans-transaction')?.canFetch).toBe(
- false
- );
- });
- it('initializes canFetch to true if no spanChildrenCount', () => {
- const tree = TraceTree.FromTrace(
- makeTrace({
- transactions: [
- makeTransaction({
- event_id: 'transaction',
- children: [],
- }),
- ],
- }),
- {meta: null, replay: null}
- );
- expect(findTransactionByEventId(tree, 'transaction')?.canFetch).toBe(true);
- });
- });
- describe('events', () => {
- it('does not dispatch timeline change when spans fall inside the trace bounds', async () => {
- const t = makeTrace({
- transactions: [
- makeTransaction({
- start_timestamp: start,
- timestamp: start + 2,
- event_id: 'event-id',
- project_slug: 'project',
- children: [],
- }),
- ],
- orphan_errors: [],
- });
- const tree = TraceTree.FromTrace(t, traceMetadata);
- const listener = jest.fn();
- tree.on('trace timeline change', listener);
- const txn = TraceTree.Find(tree.root, n => isTransactionNode(n))!;
- mockSpansResponse(
- [makeSpan({start_timestamp: start + 0.5, timestamp: start + 1})],
- 'project',
- 'event-id'
- );
- await tree.zoom(txn, true, {
- api: new MockApiClient(),
- organization: OrganizationFixture(),
- preferences: DEFAULT_TRACE_VIEW_PREFERENCES,
- });
- expect(listener).not.toHaveBeenCalled();
- });
- it('dispatches timeline change when span timestamp > trace timestamp', async () => {
- const t = makeTrace({
- transactions: [
- makeTransaction({
- start_timestamp: start,
- timestamp: start + 1,
- event_id: 'event-id',
- project_slug: 'project',
- children: [],
- }),
- ],
- orphan_errors: [],
- });
- const tree = TraceTree.FromTrace(t, traceMetadata);
- const listener = jest.fn();
- tree.on('trace timeline change', listener);
- const txn = TraceTree.Find(tree.root, n => isTransactionNode(n))!;
- const transactionSpaceBounds = JSON.stringify(txn.space);
- mockSpansResponse(
- [makeSpan({start_timestamp: start, timestamp: start + 1.2})],
- 'project',
- 'event-id'
- );
- await tree.zoom(txn, true, {
- api: new MockApiClient(),
- organization: OrganizationFixture(),
- preferences: DEFAULT_TRACE_VIEW_PREFERENCES,
- });
- expect(JSON.stringify(txn.space)).toEqual(transactionSpaceBounds);
- expect(listener).toHaveBeenCalledWith([start * 1e3, 1200]);
- });
- });
- describe('ForEachChild', () => {
- it('iterates dfs', () => {
- const tree = TraceTree.FromTrace(
- makeTrace({
- transactions: [
- makeTransaction({
- transaction: 'root',
- children: [
- makeTransaction({transaction: 'child'}),
- makeTransaction({transaction: 'other_child'}),
- ],
- }),
- ],
- }),
- {meta: null, replay: null}
- );
- const visitedNodes: string[] = [];
- TraceTree.ForEachChild(tree.root, node => {
- if (isTransactionNode(node)) {
- visitedNodes.push(node.value.transaction);
- }
- });
- expect(visitedNodes).toEqual(['root', 'child', 'other_child']);
- });
- });
- describe('expand', () => {
- it('expanding a parent autogroup node shows head to tail chain', () => {
- const tree = TraceTree.FromTrace(trace, traceMetadata);
- TraceTree.FromSpans(
- tree.root.children[0]!.children[0]!,
- parentAutogroupSpansWithTailChildren,
- makeEventTransaction()
- );
- TraceTree.AutogroupDirectChildrenSpanNodes(tree.root);
- const parentAutogroupNode = TraceTree.Find(tree.root, n =>
- isParentAutogroupedNode(n)
- )!;
- tree.expand(parentAutogroupNode, true);
- expect(tree.build().serialize()).toMatchSnapshot();
- });
- it('collapsing a parent autogroup node shows tail chain', () => {
- const tree = TraceTree.FromTrace(trace, traceMetadata);
- TraceTree.FromSpans(
- tree.root.children[0]!.children[0]!,
- parentAutogroupSpansWithTailChildren,
- makeEventTransaction()
- );
- TraceTree.AutogroupDirectChildrenSpanNodes(tree.root);
- const parentAutogroupNode = TraceTree.Find(tree.root, n =>
- isParentAutogroupedNode(n)
- )!;
- tree.expand(parentAutogroupNode, true);
- tree.expand(parentAutogroupNode, false);
- expect(tree.build().serialize()).toMatchSnapshot();
- });
- it('collapsing intermediary children is preserved', () => {
- const tree = TraceTree.FromTrace(trace, traceMetadata);
- TraceTree.FromSpans(
- tree.root.children[0]!.children[0]!,
- parentAutogroupSpansWithTailChildren,
- makeEventTransaction()
- );
- TraceTree.AutogroupDirectChildrenSpanNodes(tree.root);
- const parentAutogroupNode = TraceTree.Find(tree.root, n =>
- isParentAutogroupedNode(n)
- )! as ParentAutogroupNode;
- // Expand the chain and collapse an intermediary child
- tree.expand(parentAutogroupNode, true);
- tree.expand(parentAutogroupNode.head, false);
- const snapshot = tree.build().serialize();
- // Collapse the autogroup node and expand it again
- tree.expand(parentAutogroupNode, false);
- tree.expand(parentAutogroupNode, true);
- // Assert that the snapshot is preserved and we only render the parent autogroup chain
- // up to the collapsed span
- expect(tree.build().serialize()).toEqual(snapshot);
- expect(tree.build().serialize()).toMatchSnapshot();
- });
- it('expanding a sibling autogroup node shows sibling span', () => {
- const tree = TraceTree.FromTrace(trace, traceMetadata);
- TraceTree.FromSpans(
- tree.root.children[0]!.children[0]!,
- siblingAutogroupSpans,
- makeEventTransaction()
- );
- TraceTree.AutogroupSiblingSpanNodes(tree.root);
- TraceTree.ForEachChild(tree.root, n => {
- if (isSiblingAutogroupedNode(n)) {
- tree.expand(n, true);
- }
- });
- expect(tree.build().serialize()).toMatchSnapshot();
- });
- it('collapsing a sibling autogroup node hides children', () => {
- const tree = TraceTree.FromTrace(trace, traceMetadata);
- TraceTree.FromSpans(
- tree.root.children[0]!.children[0]!,
- siblingAutogroupSpans,
- makeEventTransaction()
- );
- TraceTree.AutogroupSiblingSpanNodes(tree.root);
- TraceTree.ForEachChild(tree.root, n => {
- if (isSiblingAutogroupedNode(n)) {
- tree.expand(n, true);
- }
- });
- TraceTree.ForEachChild(tree.root, n => {
- if (isSiblingAutogroupedNode(n)) {
- tree.expand(n, false);
- }
- });
- expect(tree.build().serialize()).toMatchSnapshot();
- });
- });
- describe('zoom', () => {
- it('does nothing if node cannot fetch', () => {
- const tree = TraceTree.FromTrace(traceWithEventId, traceMetadata);
- const request = mockSpansResponse([], 'project', 'event-id');
- tree.root.children[0]!.children[0]!.canFetch = false;
- tree.zoom(tree.root.children[0]!.children[0]!, true, {
- api: new MockApiClient(),
- organization: OrganizationFixture(),
- preferences: DEFAULT_TRACE_VIEW_PREFERENCES,
- });
- expect(request).not.toHaveBeenCalled();
- });
- it('caches promise', () => {
- const tree = TraceTree.FromTrace(traceWithEventId, traceMetadata);
- const request = mockSpansResponse([], 'project', 'event-id');
- tree.zoom(tree.root.children[0]!.children[0]!, true, {
- api: new MockApiClient(),
- organization: OrganizationFixture(),
- preferences: DEFAULT_TRACE_VIEW_PREFERENCES,
- });
- tree.zoom(tree.root.children[0]!, true, {
- api: new MockApiClient(),
- organization: OrganizationFixture(),
- preferences: DEFAULT_TRACE_VIEW_PREFERENCES,
- });
- expect(request).toHaveBeenCalledTimes(1);
- });
- it('zooms in on transaction node', async () => {
- const tree = TraceTree.FromTrace(traceWithEventId, traceMetadata);
- mockSpansResponse([makeSpan()], 'project', 'child-event-id');
- // Zoom mutates the list, so we need to build first
- tree.build();
- await tree.zoom(tree.root.children[0]!.children[0]!.children[0]!, true, {
- api: new MockApiClient(),
- organization: OrganizationFixture(),
- preferences: DEFAULT_TRACE_VIEW_PREFERENCES,
- });
- expect(tree.build().serialize()).toMatchSnapshot();
- });
- it('maintains the span tree when parent is zoomed in', async () => {
- const tree = TraceTree.FromTrace(traceWithEventId, traceMetadata);
- // Zoom mutates the list, so we need to build first
- tree.build();
- // Zoom in on child span
- mockSpansResponse([makeSpan()], 'project', 'child-event-id');
- await tree.zoom(tree.root.children[0]!.children[0]!.children[0]!, true, {
- api: new MockApiClient(),
- organization: OrganizationFixture(),
- preferences: DEFAULT_TRACE_VIEW_PREFERENCES,
- });
- // Then zoom in on a parent
- mockSpansResponse([makeSpan()], 'project', 'event-id');
- await tree.zoom(tree.root.children[0]!.children[0]!, true, {
- api: new MockApiClient(),
- organization: OrganizationFixture(),
- preferences: DEFAULT_TRACE_VIEW_PREFERENCES,
- });
- expect(tree.build().serialize()).toMatchSnapshot();
- });
- it('reparents child transactions under spans with matching ids', async () => {
- const tree = TraceTree.FromTrace(
- makeTrace({
- transactions: [
- makeTransaction({
- transaction: 'root',
- event_id: 'parent-event-id',
- project_slug: 'project',
- children: [
- makeTransaction({
- transaction: 'child',
- parent_span_id: '0000',
- event_id: 'child-event-id',
- project_slug: 'project',
- }),
- ],
- }),
- ],
- }),
- traceMetadata
- );
- // Zoom mutates the list, so we need to build first
- tree.build();
- mockSpansResponse([makeSpan({span_id: '0001'})], 'project', 'child-event-id');
- await tree.zoom(tree.root.children[0]!.children[0]!.children[0]!, true, {
- api: new MockApiClient(),
- organization: OrganizationFixture(),
- preferences: DEFAULT_TRACE_VIEW_PREFERENCES,
- });
- mockSpansResponse([makeSpan({span_id: '0000'})], 'project', 'parent-event-id');
- await tree.zoom(tree.root.children[0]!.children[0]!, true, {
- api: new MockApiClient(),
- organization: OrganizationFixture(),
- preferences: DEFAULT_TRACE_VIEW_PREFERENCES,
- });
- expect(tree.build().serialize()).toMatchSnapshot();
- });
- it('preserves parent of nested child transactions', async () => {
- const tree = TraceTree.FromTrace(
- makeTrace({
- transactions: [
- makeTransaction({
- transaction: 'root',
- event_id: 'parent-event-id',
- project_slug: 'project',
- children: [
- makeTransaction({
- transaction: 'child',
- event_id: 'child-event-id',
- project_slug: 'project',
- parent_span_id: '0000',
- children: [
- makeTransaction({
- transaction: 'grandchild',
- event_id: 'grandchild-event-id',
- project_slug: 'project',
- }),
- ],
- }),
- ],
- }),
- ],
- }),
- traceMetadata
- );
- // Zoom mutates the list, so we need to build first
- tree.build();
- mockSpansResponse([makeSpan({span_id: '0000'})], 'project', 'parent-event-id');
- await tree.zoom(tree.root.children[0]!.children[0]!, true, {
- api: new MockApiClient(),
- organization: OrganizationFixture(),
- preferences: DEFAULT_TRACE_VIEW_PREFERENCES,
- });
- const grandchild = findTransactionByEventId(tree, 'grandchild-event-id');
- const child = findTransactionByEventId(tree, 'child-event-id');
- expect(grandchild?.parent).toBe(child);
- expect(tree.serialize()).toMatchSnapshot();
- });
- it('zoomout returns tree back to a transaction tree', async () => {
- const tree = TraceTree.FromTrace(
- makeTrace({
- transactions: [
- makeTransaction({
- transaction: 'root',
- event_id: 'parent-event-id',
- project_slug: 'project',
- children: [
- makeTransaction({
- transaction: 'child',
- event_id: 'child-event-id',
- project_slug: 'project',
- parent_span_id: '0000',
- children: [
- makeTransaction({
- transaction: 'grandchild',
- event_id: 'grandchild-event-id',
- project_slug: 'project',
- }),
- ],
- }),
- ],
- }),
- ],
- }),
- traceMetadata
- );
- // Zoom mutates the list, so we need to build first
- const transactionTreeSnapshot = tree.build().serialize();
- mockSpansResponse([makeSpan({span_id: '0000'})], 'project', 'parent-event-id');
- for (const bool of [true, false]) {
- await tree.zoom(tree.root.children[0]!.children[0]!, bool, {
- api: new MockApiClient(),
- organization: OrganizationFixture(),
- preferences: DEFAULT_TRACE_VIEW_PREFERENCES,
- });
- }
- expect(tree.serialize()).toEqual(transactionTreeSnapshot);
- });
- // @TODO This currently filters out all spans - we should preserve spans that are children of other
- // zoomed in transactions
- it('zooming out preserves spans of child zoomed in transaction', async () => {
- const tree = TraceTree.FromTrace(
- makeTrace({
- transactions: [
- makeTransaction({
- transaction: 'root',
- event_id: 'parent-event-id',
- project_slug: 'project',
- children: [
- makeTransaction({
- transaction: 'child',
- event_id: 'child-event-id',
- project_slug: 'project',
- children: [
- makeTransaction({
- transaction: 'grandchild',
- event_id: 'grandchild-event-id',
- project_slug: 'project',
- parent_span_id: '0000',
- }),
- ],
- }),
- ],
- }),
- ],
- }),
- traceMetadata
- );
- // Zoom mutates the list, so we need to build first
- tree.build();
- mockSpansResponse(
- [makeSpan({span_id: '0000', op: 'parent-op'})],
- 'project',
- 'child-event-id'
- );
- const child = findTransactionByEventId(tree, 'child-event-id');
- await tree.zoom(child!, true, {
- api: new MockApiClient(),
- organization: OrganizationFixture(),
- preferences: DEFAULT_TRACE_VIEW_PREFERENCES,
- });
- mockSpansResponse(
- [makeSpan({span_id: '0001', op: 'child-op'})],
- 'project',
- 'grandchild-event-id'
- );
- const grandchild = findTransactionByEventId(tree, 'grandchild-event-id');
- await tree.zoom(grandchild!, true, {
- api: new MockApiClient(),
- organization: OrganizationFixture(),
- preferences: DEFAULT_TRACE_VIEW_PREFERENCES,
- });
- await tree.zoom(child!, false, {
- api: new MockApiClient(),
- organization: OrganizationFixture(),
- preferences: DEFAULT_TRACE_VIEW_PREFERENCES,
- });
- const spans = TraceTree.FindAll(tree.root, n => isSpanNode(n));
- expect(spans).toHaveLength(1);
- expect(tree.serialize()).toMatchSnapshot();
- });
- });
- describe('Find', () => {
- it('finds first node by predicate', () => {
- const tree = TraceTree.FromTrace(
- makeTrace({
- transactions: [
- makeTransaction({
- transaction: 'first',
- children: [makeTransaction({transaction: 'second'})],
- }),
- ],
- }),
- traceMetadata
- );
- const node = TraceTree.Find(tree.root, n => isTransactionNode(n));
- expect(node).not.toBeNull();
- expect((node as any).value.transaction).toBe('first');
- });
- it('returns null if no node is found', () => {
- const tree = TraceTree.FromTrace(trace, traceMetadata);
- const node = TraceTree.Find(tree.root, n => (n as any) === 'does not exist');
- expect(node).toBeNull();
- });
- });
- describe('FindByID', () => {
- it('finds transaction by event_id', () => {
- const traceWithError = makeTrace({
- transactions: [
- makeTransaction({transaction: 'first', event_id: 'first-event-id'}),
- ],
- });
- const tree = TraceTree.FromTrace(traceWithError, traceMetadata);
- const node = TraceTree.FindByID(tree.root, 'first-event-id');
- assertTransactionNode(node);
- expect(node.value.transaction).toBe('first');
- });
- it('matches by error event_id', () => {
- const traceWithError = makeTrace({
- transactions: [
- makeTransaction({
- transaction: 'first',
- event_id: 'txn-event-id',
- errors: [makeTraceError({event_id: 'error-event-id'})],
- }),
- ],
- });
- const tree = TraceTree.FromTrace(traceWithError, traceMetadata);
- const node = TraceTree.FindByID(tree.root, 'error-event-id');
- assertTransactionNode(node);
- expect(node.value.transaction).toBe('first');
- });
- });
- describe('FindAll', () => {
- it('finds all nodes by predicate', () => {
- const tree = TraceTree.FromTrace(trace, traceMetadata);
- const nodes = TraceTree.FindAll(tree.root, n => isTransactionNode(n));
- expect(nodes).toHaveLength(2);
- });
- });
- describe('DirectVisibleChildren', () => {
- it('returns children for transaction', () => {
- const tree = TraceTree.FromTrace(trace, traceMetadata);
- expect(TraceTree.DirectVisibleChildren(tree.root.children[0]!)).toEqual(
- tree.root.children[0]!.children
- );
- });
- it('returns tail for collapsed parent autogroup', () => {
- const tree = TraceTree.FromTrace(trace, traceMetadata);
- TraceTree.FromSpans(
- tree.root.children[0]!,
- parentAutogroupSpansWithTailChildren,
- makeEventTransaction()
- );
- TraceTree.AutogroupDirectChildrenSpanNodes(tree.root);
- const parentAutogroup = TraceTree.Find(tree.root, node =>
- isParentAutogroupedNode(node)
- ) as ParentAutogroupNode;
- expect(parentAutogroup).not.toBeNull();
- expect(TraceTree.DirectVisibleChildren(parentAutogroup)[0]).toBe(
- parentAutogroup.tail.children[0]
- );
- });
- it('returns head for expanded parent autogroup', () => {
- const tree = TraceTree.FromTrace(trace, traceMetadata);
- TraceTree.FromSpans(
- tree.root.children[0]!,
- parentAutogroupSpans,
- makeEventTransaction()
- );
- TraceTree.AutogroupDirectChildrenSpanNodes(tree.root);
- const parentAutogroup = TraceTree.Find(tree.root, node =>
- isParentAutogroupedNode(node)
- ) as ParentAutogroupNode;
- tree.expand(parentAutogroup, true);
- expect(TraceTree.DirectVisibleChildren(parentAutogroup)[0]).toBe(
- parentAutogroup.head
- );
- });
- });
- describe('HasVisibleChildren', () => {
- it('true when transaction has children', () => {
- const tree = TraceTree.FromTrace(
- makeTrace({
- transactions: [makeTransaction({children: [makeTransaction()]})],
- }),
- traceMetadata
- );
- expect(TraceTree.HasVisibleChildren(tree.root.children[0]!)).toBe(true);
- });
- describe('span', () => {
- it.each([true, false])('%s when span has children and is expanded', expanded => {
- const tree = TraceTree.FromTrace(
- makeTrace({
- transactions: [makeTransaction({children: [makeTransaction()]})],
- }),
- traceMetadata
- );
- TraceTree.FromSpans(
- tree.root.children[0]!,
- [
- makeSpan({span_id: '0000'}),
- makeSpan({span_id: '0001', parent_span_id: '0000'}),
- ],
- makeEventTransaction()
- );
- const span = TraceTree.Find(
- tree.root,
- node => isSpanNode(node) && node.value.span_id === '0000'
- )!;
- tree.expand(span, expanded);
- expect(TraceTree.HasVisibleChildren(span)).toBe(expanded);
- });
- });
- describe('sibling autogroup', () => {
- it.each([true, false])('%s when sibling autogroup is expanded', expanded => {
- const tree = TraceTree.FromTrace(trace, traceMetadata);
- TraceTree.FromSpans(
- tree.root.children[0]!,
- siblingAutogroupSpans,
- makeEventTransaction()
- );
- TraceTree.AutogroupSiblingSpanNodes(tree.root);
- const siblingAutogroup = TraceTree.Find(tree.root, node =>
- isSiblingAutogroupedNode(node)
- );
- tree.expand(siblingAutogroup!, expanded);
- expect(TraceTree.HasVisibleChildren(siblingAutogroup!)).toBe(expanded);
- });
- });
- describe('parent autogroup', () => {
- it.each([true, false])('%s when parent autogroup is expanded', expanded => {
- const tree = TraceTree.FromTrace(trace, traceMetadata);
- TraceTree.FromSpans(
- tree.root.children[0]!,
- parentAutogroupSpans,
- makeEventTransaction()
- );
- TraceTree.AutogroupDirectChildrenSpanNodes(tree.root);
- const parentAutogroup = TraceTree.Find(tree.root, node =>
- isParentAutogroupedNode(node)
- );
- tree.expand(parentAutogroup!, expanded);
- expect(TraceTree.HasVisibleChildren(parentAutogroup!)).toBe(expanded);
- });
- });
- describe('parent autogroup when tail has children', () => {
- // Always true because tail has children
- it.each([true, false])('%s when parent autogroup is expanded', expanded => {
- const tree = TraceTree.FromTrace(trace, traceMetadata);
- TraceTree.FromSpans(
- tree.root.children[0]!,
- parentAutogroupSpansWithTailChildren,
- makeEventTransaction()
- );
- TraceTree.AutogroupDirectChildrenSpanNodes(tree.root);
- tree.build();
- const parentAutogroup = TraceTree.Find(tree.root, node =>
- isParentAutogroupedNode(node)
- );
- tree.expand(parentAutogroup!, expanded);
- expect(TraceTree.HasVisibleChildren(parentAutogroup!)).toBe(true);
- });
- });
- });
- describe('IsLastChild', () => {
- it('returns false if node is not last child', () => {
- const tree = TraceTree.FromTrace(
- makeTrace({
- transactions: [makeTransaction(), makeTransaction()],
- }),
- traceMetadata
- );
- expect(TraceTree.IsLastChild(tree.root.children[0]!.children[0]!)).toBe(false);
- });
- it('returns true if node is last child', () => {
- const tree = TraceTree.FromTrace(
- makeTrace({
- transactions: [makeTransaction(), makeTransaction()],
- }),
- traceMetadata
- );
- expect(TraceTree.IsLastChild(tree.root.children[0]!.children[1]!)).toBe(true);
- });
- });
- describe('Invalidate', () => {
- it('invalidates node', () => {
- const tree = TraceTree.FromTrace(trace, traceMetadata);
- tree.root.children[0]!.depth = 10;
- tree.root.children[0]!.connectors = [1, 2, 3];
- TraceTree.invalidate(tree.root.children[0]!, false);
- expect(tree.root.children[0]!.depth).toBeUndefined();
- expect(tree.root.children[0]!.connectors).toBeUndefined();
- });
- it('recursively invalidates children', () => {
- const tree = TraceTree.FromTrace(trace, traceMetadata);
- tree.root.children[0]!.depth = 10;
- tree.root.children[0]!.connectors = [1, 2, 3];
- TraceTree.invalidate(tree.root, true);
- expect(tree.root.children[0]!.depth).toBeUndefined();
- expect(tree.root.children[0]!.connectors).toBeUndefined();
- });
- });
- describe('appendTree', () => {
- it('appends tree to end of current tree', () => {
- const tree = TraceTree.FromTrace(trace, {replay: null, meta: null});
- tree.appendTree(TraceTree.FromTrace(trace, {replay: null, meta: null}));
- expect(tree.build().serialize()).toMatchSnapshot();
- });
- it('appending extends trace space', () => {
- const tree = TraceTree.FromTrace(
- makeTrace({
- transactions: [makeTransaction({start_timestamp: start, timestamp: start + 1})],
- }),
- {replay: null, meta: null}
- );
- const otherTree = TraceTree.FromTrace(
- makeTrace({
- transactions: [
- makeTransaction({start_timestamp: start, timestamp: start + 10}),
- ],
- }),
- {replay: null, meta: null}
- );
- tree.appendTree(otherTree);
- expect(tree.root.space[0]).toBe(start * 1e3);
- expect(tree.root.space[1]).toBe(10 * 1e3);
- });
- });
- describe('PathToNode', () => {
- const nestedTransactionTrace = makeTrace({
- transactions: [
- makeTransaction({
- start_timestamp: start,
- timestamp: start + 2,
- transaction: 'parent',
- span_id: 'parent-span-id',
- event_id: 'parent-event-id',
- children: [
- makeTransaction({
- start_timestamp: start + 1,
- timestamp: start + 4,
- transaction: 'child',
- event_id: 'child-event-id',
- }),
- ],
- }),
- ],
- });
- it('path to transaction node', () => {
- const tree = TraceTree.FromTrace(nestedTransactionTrace, traceMetadata);
- const transactionNode = TraceTree.Find(
- tree.root,
- node => isTransactionNode(node) && node.value.transaction === 'child'
- )!;
- const path = TraceTree.PathToNode(transactionNode);
- expect(path).toEqual(['txn-child-event-id']);
- });
- it('path to span includes parent txn', () => {
- const tree = TraceTree.FromTrace(nestedTransactionTrace, traceMetadata);
- const child = TraceTree.Find(
- tree.root,
- node => isTransactionNode(node) && node.value.transaction === 'child'
- )!;
- TraceTree.FromSpans(
- child,
- [makeSpan({span_id: 'span-id'})],
- makeEventTransaction()
- );
- const span = TraceTree.Find(tree.root, node => isSpanNode(node))!;
- const path = TraceTree.PathToNode(span);
- expect(path).toEqual(['span-span-id', 'txn-child-event-id']);
- });
- describe('parent autogroup', () => {
- const pathParentAutogroupSpans = [
- makeSpan({op: 'db', description: 'redis', span_id: 'head-span-id'}),
- makeSpan({
- op: 'db',
- description: 'redis',
- span_id: 'tail-span-id',
- parent_span_id: 'head-span-id',
- }),
- makeSpan({
- op: 'http',
- description: 'request',
- span_id: 'child-span-id',
- parent_span_id: 'tail-span-id',
- }),
- ];
- it('parent autogroup', () => {
- const tree = TraceTree.FromTrace(nestedTransactionTrace, traceMetadata);
- const child = TraceTree.Find(
- tree.root,
- node => isTransactionNode(node) && node.value.transaction === 'child'
- )!;
- TraceTree.FromSpans(child, pathParentAutogroupSpans, makeEventTransaction());
- TraceTree.AutogroupDirectChildrenSpanNodes(tree.root);
- const parentAutogroup = TraceTree.Find(tree.root, node =>
- isParentAutogroupedNode(node)
- )!;
- const path = TraceTree.PathToNode(parentAutogroup);
- expect(path).toEqual(['ag-head-span-id', 'txn-child-event-id']);
- });
- it('path to child of parent autogroup skips autogroup', () => {
- const tree = TraceTree.FromTrace(nestedTransactionTrace, traceMetadata);
- const child = TraceTree.Find(
- tree.root,
- node => isTransactionNode(node) && node.value.transaction === 'child'
- )!;
- TraceTree.FromSpans(child, pathParentAutogroupSpans, makeEventTransaction());
- TraceTree.AutogroupDirectChildrenSpanNodes(tree.root);
- const parentAutogroup = TraceTree.Find(tree.root, node =>
- isParentAutogroupedNode(node)
- ) as ParentAutogroupNode;
- expect(TraceTree.PathToNode(parentAutogroup.tail)).toEqual([
- 'span-tail-span-id',
- 'txn-child-event-id',
- ]);
- const requestSpan = TraceTree.Find(
- tree.root,
- node => isSpanNode(node) && node.value.description === 'request'
- )!;
- expect(TraceTree.PathToNode(requestSpan)).toEqual([
- 'span-child-span-id',
- 'txn-child-event-id',
- ]);
- });
- });
- describe('sibling autogroup', () => {
- const pathSiblingAutogroupSpans = [
- makeSpan({
- op: 'db',
- description: 'redis',
- span_id: '0',
- start_timestamp: start,
- timestamp: start + 1,
- }),
- makeSpan({
- op: 'db',
- description: 'redis',
- start_timestamp: start,
- timestamp: start + 1,
- span_id: '1',
- }),
- makeSpan({
- op: 'db',
- description: 'redis',
- start_timestamp: start,
- timestamp: start + 1,
- }),
- makeSpan({
- op: 'db',
- description: 'redis',
- start_timestamp: start,
- timestamp: start + 1,
- }),
- makeSpan({
- op: 'db',
- description: 'redis',
- start_timestamp: start,
- timestamp: start + 1,
- }),
- ];
- it('path to sibling autogroup', () => {
- const tree = TraceTree.FromTrace(nestedTransactionTrace, traceMetadata);
- const child = TraceTree.Find(
- tree.root,
- node => isTransactionNode(node) && node.value.transaction === 'child'
- )!;
- TraceTree.FromSpans(child, pathSiblingAutogroupSpans, makeEventTransaction());
- TraceTree.AutogroupSiblingSpanNodes(tree.root);
- const siblingAutogroup = TraceTree.Find(tree.root, node =>
- isSiblingAutogroupedNode(node)
- ) as SiblingAutogroupNode;
- const path = TraceTree.PathToNode(siblingAutogroup);
- expect(path).toEqual(['ag-0', 'txn-child-event-id']);
- });
- it('path to child of sibling autogroup skips autogroup', () => {
- const tree = TraceTree.FromTrace(nestedTransactionTrace, traceMetadata);
- const child = TraceTree.Find(
- tree.root,
- node => isTransactionNode(node) && node.value.transaction === 'child'
- )!;
- TraceTree.FromSpans(child, pathSiblingAutogroupSpans, makeEventTransaction());
- TraceTree.AutogroupSiblingSpanNodes(tree.root);
- const siblingAutogroup = TraceTree.Find(tree.root, node =>
- isSiblingAutogroupedNode(node)
- ) as SiblingAutogroupNode;
- const path = TraceTree.PathToNode(siblingAutogroup.children[1]!);
- expect(path).toEqual(['span-1', 'txn-child-event-id']);
- });
- });
- it('path to missing instrumentation node', () => {
- const tree = TraceTree.FromTrace(nestedTransactionTrace, traceMetadata);
- const missingInstrumentationSpans = [
- makeSpan({
- op: 'db',
- description: 'redis',
- span_id: '0',
- start_timestamp: start,
- timestamp: start + 1,
- }),
- makeSpan({
- op: 'db',
- description: 'redis',
- start_timestamp: start + 2,
- timestamp: start + 4,
- }),
- ];
- const child = TraceTree.Find(
- tree.root,
- node => isTransactionNode(node) && node.value.transaction === 'child'
- )!;
- TraceTree.FromSpans(child, missingInstrumentationSpans, makeEventTransaction());
- TraceTree.DetectMissingInstrumentation(tree.root);
- const missingInstrumentationNode = TraceTree.Find(tree.root, node =>
- isMissingInstrumentationNode(node)
- )!;
- const path = TraceTree.PathToNode(missingInstrumentationNode);
- expect(path).toEqual(['ms-0', 'txn-child-event-id']);
- });
- });
- describe('ExpandToPath', () => {
- const organization = OrganizationFixture();
- const api = new MockApiClient();
- const nestedTransactionTrace = makeTrace({
- transactions: [
- makeTransaction({
- start_timestamp: start,
- timestamp: start + 2,
- transaction: 'parent',
- span_id: 'parent-span-id',
- event_id: 'parent-event-id',
- children: [
- makeTransaction({
- start_timestamp: start + 1,
- timestamp: start + 4,
- transaction: 'child',
- event_id: 'child-event-id',
- project_slug: 'project',
- }),
- ],
- }),
- ],
- });
- it('expands transactions from path segments', async () => {
- const tree = TraceTree.FromTrace(nestedTransactionTrace, traceMetadata);
- const child = TraceTree.Find(
- tree.root,
- node => isTransactionNode(node) && node.value.transaction === 'child'
- )!;
- await TraceTree.ExpandToPath(tree, TraceTree.PathToNode(child), {
- api,
- organization,
- preferences: DEFAULT_TRACE_VIEW_PREFERENCES,
- });
- expect(tree.build().serialize()).toMatchSnapshot();
- });
- it('discards non txns segments', async () => {
- const tree = TraceTree.FromTrace(nestedTransactionTrace, traceMetadata);
- const child = TraceTree.Find(
- tree.root,
- node => isTransactionNode(node) && node.value.transaction === 'child'
- )!;
- const request = mockSpansResponse([makeSpan()], 'project', 'child-event-id');
- await TraceTree.ExpandToPath(tree, ['span-0', ...TraceTree.PathToNode(child)], {
- api,
- organization,
- preferences: DEFAULT_TRACE_VIEW_PREFERENCES,
- });
- expect(request).toHaveBeenCalled();
- expect(tree.build().serialize()).toMatchSnapshot();
- });
- });
- });
|