Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
import {
SEARCH_PAGE__SERVER_RENDERING__TYPE,
SEARCH_SENTRY__LINK__TYPE,
} from '@sentry/conventions/attributes/search';

import type {
TraceItemResponseAttribute,
TraceItemResponseLink,
} from 'sentry/views/explore/hooks/useTraceItemDetails';

import {getPageServerRendering} from './serverRendering';

function typeAttribute(value: string): TraceItemResponseAttribute[] {
return [{name: SEARCH_PAGE__SERVER_RENDERING__TYPE, type: 'str', value}];
}

function makeLink(
type: string,
overrides: Partial<TraceItemResponseLink> = {}
): TraceItemResponseLink {
return {
itemId: 'b415e097df49bf1c',
traceId: '6edf623ed48e4172a54e7fbee3b40c5d',
sampled: true,
attributes: [{name: SEARCH_SENTRY__LINK__TYPE, type: 'str', value: type}],
...overrides,
};
}

describe('getPageServerRendering', () => {
it('returns null for a span that is not a pageload', () => {
const result = getPageServerRendering({
op: 'http.server',
attributes: typeAttribute('static'),
links: [makeLink('cache_origin')],
});

expect(result).toBeNull();
});

it('returns null for a pageload whose links are not about server rendering', () => {
const result = getPageServerRendering({
op: 'pageload',
attributes: [],
links: [makeLink('previous_trace')],
});

expect(result).toBeNull();
});

it('returns null for a type value that SDKs do not set, such as dynamic', () => {
const result = getPageServerRendering({
op: 'pageload',
attributes: typeAttribute('dynamic'),
});

expect(result).toBeNull();
});

it('returns the whole page as a shared render in the linked trace for a static page', () => {
const cacheOrigin = makeLink('cache_origin');

const result = getPageServerRendering({
op: 'pageload',
attributes: typeAttribute('static'),
links: [cacheOrigin],
});

expect(result).toEqual({
type: 'static',
sharedRender: {
part: 'whole_page',
location: {type: 'other_trace', link: cacheOrigin},
},
requestRender: undefined,
});
});

it('marks the shared render as not traced when a static page has no link', () => {
const result = getPageServerRendering({
op: 'pageload',
attributes: typeAttribute('static'),
});

expect(result).toEqual({
type: 'static',
sharedRender: {part: 'whole_page', location: {type: 'not_traced'}},
requestRender: undefined,
});
});

it('marks a link to an unsampled render as not sampled', () => {
const cacheOrigin = makeLink('cache_origin', {sampled: false});

const result = getPageServerRendering({
op: 'pageload',
attributes: typeAttribute('static'),
links: [cacheOrigin],
});

expect(result?.sharedRender).toEqual({
part: 'whole_page',
location: {type: 'not_sampled', link: cacheOrigin},
});
});

it('treats a link without a sampling decision as sampled', () => {
const cacheOrigin = makeLink('cache_origin', {sampled: undefined});

const result = getPageServerRendering({
op: 'pageload',
attributes: typeAttribute('static'),
links: [cacheOrigin],
});

expect(result?.sharedRender).toEqual({
part: 'whole_page',
location: {type: 'other_trace', link: cacheOrigin},
});
});

it('puts the dynamic part of a partially static page in this trace', () => {
const cacheOrigin = makeLink('cache_origin');

const result = getPageServerRendering({
op: 'pageload',
attributes: typeAttribute('partially_static'),
links: [cacheOrigin],
});

expect(result).toEqual({
type: 'partially_static',
sharedRender: {
part: 'static_part',
location: {type: 'other_trace', link: cacheOrigin},
},
requestRender: {location: {type: 'this_trace'}},
});
});

it('puts the dynamic part in the linked trace when its trace context arrived late', () => {
const serverRequest = makeLink('server_request', {
traceId: '7fe0734fe59f5283b65f8acff4c51d6e',
});

const result = getPageServerRendering({
op: 'pageload',
attributes: typeAttribute('partially_static'),
links: [makeLink('cache_origin'), serverRequest],
});

expect(result?.requestRender).toEqual({
location: {type: 'other_trace', link: serverRequest},
});
});

it('returns a shared render of an unknown part when only the link says the page was reused', () => {
const cacheOrigin = makeLink('cache_origin');
const serverRequest = makeLink('server_request', {
traceId: '7fe0734fe59f5283b65f8acff4c51d6e',
});

const result = getPageServerRendering({
op: 'pageload',
attributes: [],
links: [serverRequest, cacheOrigin],
});

expect(result).toEqual({
type: undefined,
sharedRender: {part: undefined, location: {type: 'other_trace', link: cacheOrigin}},
requestRender: {location: {type: 'other_trace', link: serverRequest}},
});
});

it('returns only the request render for a late trace context without reuse', () => {
const serverRequest = makeLink('server_request', {sampled: false});

const result = getPageServerRendering({
op: 'pageload',
attributes: [],
links: [serverRequest],
});

expect(result).toEqual({
type: undefined,
sharedRender: undefined,
requestRender: {location: {type: 'not_sampled', link: serverRequest}},
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import {SEARCH_PAGE__SERVER_RENDERING__TYPE} from '@sentry/conventions/attributes/search';

import {getAttributeValue} from 'sentry/utils/fields/getAttributeValue';
import type {
TraceItemResponseAttribute,
TraceItemResponseLink,
} from 'sentry/views/explore/hooks/useTraceItemDetails';
import {getSpanLinkType} from 'sentry/views/performance/traceDetails/getSpanLinkType';

/**
* How the server rendered the page. Not set means rendered for this request,
* or not known.
* https://conventions.sentry.dev/attributes/page/#page-server_rendering-type
*/
export type ServerRenderingType = 'static' | 'partially_static';

/** The part of the page that a shared render made. */
export type StaticPart = 'whole_page' | 'static_part';

/** Where the spans of a render are, seen from the pageload span. */
type RenderLocation =
| {type: 'this_trace'}
| {link: TraceItemResponseLink; type: 'other_trace'}
// The linked trace was not sampled, so it has no spans.
| {link: TraceItemResponseLink; type: 'not_sampled'}
// The page was reused, but no link points to its render.
| {type: 'not_traced'};

export interface ServerRender {
location: RenderLocation;
}

/** A render whose HTML the server reuses for many page loads. */
interface SharedRender extends ServerRender {
/** `undefined` when the type is not set. */
part: StaticPart | undefined;
}

/** At least one render is set. A page can have both. */
interface PageServerRendering {
type: ServerRenderingType | undefined;
/** The render for this page load's request. */
requestRender?: ServerRender;
sharedRender?: SharedRender;
}

const PAGELOAD_OP = 'pageload';
const CACHE_ORIGIN_LINK_TYPE = 'cache_origin';
const SERVER_REQUEST_LINK_TYPE = 'server_request';

function getServerRenderingType(
value: string | number | boolean | undefined
): ServerRenderingType | undefined {
return value === 'static' || value === 'partially_static' ? value : undefined;
}

function toLinkedLocation(link: TraceItemResponseLink): RenderLocation {
// A link without a sampling decision counts as sampled.
return link.sampled === false
? {type: 'not_sampled', link}
: {type: 'other_trace', link};
}

function getStaticPart(type: ServerRenderingType): StaticPart {
return type === 'static' ? 'whole_page' : 'static_part';
}

/**
* The server renders that made a pageload's page, and where their spans are.
* Uses only span attributes and links, so it works for every SDK and framework.
* Returns `null` for other spans and for pages rendered only for this request.
*/
export function getPageServerRendering({
op,
attributes,
links,
}: {
attributes: TraceItemResponseAttribute[];
op: string | undefined;
links?: TraceItemResponseLink[];
}): PageServerRendering | null {
if (op !== PAGELOAD_OP) {
return null;
}

const type = getServerRenderingType(
getAttributeValue(attributes, SEARCH_PAGE__SERVER_RENDERING__TYPE, 'string')
);
const cacheOrigin = findLinkOfType(links, CACHE_ORIGIN_LINK_TYPE);
const serverRequest = findLinkOfType(links, SERVER_REQUEST_LINK_TYPE);

const sharedRender = getSharedRender(type, cacheOrigin);
const requestRender = getRequestRender(type, serverRequest);

if (!sharedRender && !requestRender) {
return null;
}
return {type, sharedRender, requestRender};
}

function findLinkOfType(
links: TraceItemResponseLink[] | undefined,
type: string
): TraceItemResponseLink | undefined {
return links?.find(link => getSpanLinkType(link) === type);
}

/** A `cache_origin` link or a static type means the server reused HTML. */
function getSharedRender(
type: ServerRenderingType | undefined,
cacheOrigin: TraceItemResponseLink | undefined
): SharedRender | undefined {
if (!type && !cacheOrigin) {
return undefined;
}
return {
part: type ? getStaticPart(type) : undefined,
location: cacheOrigin ? toLinkedLocation(cacheOrigin) : {type: 'not_traced'},
};
}

/**
* A `server_request` link means the request has its own trace, because its
* trace ID arrived after the pageload started.
*/
function getRequestRender(
type: ServerRenderingType | undefined,
serverRequest: TraceItemResponseLink | undefined
): ServerRender | undefined {
if (serverRequest) {
return {location: toLinkedLocation(serverRequest)};
}
// The dynamic part of a partially static page renders for each request.
return type === 'partially_static' ? {location: {type: 'this_trace'}} : undefined;
}
Loading
Loading