Skip to content
Open
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
79 changes: 79 additions & 0 deletions docs/embed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Embed Routes

Public iframe-embedding surface for InferenceX charts. Partner sites can embed any supported chart by iframing an `/embed/*` URL.

## URL parameter contract

Embed URLs use **the same `g_*` / `i_*` parameter keys as the main `/inference` site** — there is no separate embed-specific key contract to maintain. If a site key is renamed or a new key is added, the embed URL automatically benefits from the change. The only embed-specific key is `i_chart` (which chart variant to display — the main site renders both E2E and interactivity together, embeds show only one).

## Supported routes

| Route | Chart |
| ---------------- | ---------------------------------------- |
| `/embed/scatter` | Scatter (E2E throughput / interactivity) |

## `/embed/scatter`

### URL shape

```
/embed/scatter?g_model=DeepSeek-R1-0528&i_seq=8k%2F1k&i_prec=fp4
&i_metric=y_tpPerGpu&i_active=b200_sglang,gb300_dynamo-sglang&i_chart=e2e
```

### Parameters

| Key | Type | Default | Notes |
| ---------- | ------ | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `g_model` | string | `DeepSeek-R1-0528` | Display model name — same as `g_model` on the main site. |
| `i_seq` | string | `8k/1k` | Sequence string (e.g. `8k/1k`, `1k/1k`, `1k/8k`) — same as `i_seq` on the main site. |
| `i_prec` | string | `fp4` | Comma-separated precision keys (e.g. `fp4`, `fp8`, `bf16`) — same as `i_prec` on the main site. |
| `i_metric` | string | `y_tpPerGpu` | Y-axis metric key (e.g. `y_tpPerGpu`, `y_costh`) — same as `i_metric` on the main site. |
| `i_active` | string | `` (all visible) | Comma-separated hwKey allow-list (e.g. `b200_sglang,gb300_dynamo-sglang`). When set, the embed legend and chart universe are restricted to exactly these GPUs. Viewers can toggle them on/off but cannot add GPUs outside this set. When absent, all GPUs for the selected model/sequence/precision are shown. |
| `i_chart` | string | `e2e` | Chart variant to render: `e2e` or `interactivity`. Embed-only key — the main site renders both charts together. |

All other `g_*` / `i_*` keys recognized by the main site (e.g. `i_scale`, `i_hc`, `i_nolabel`) are passed through as-is and respected by the embed — the provider stack is identical. Unknown keys are silently ignored.

### `i_active` — hwKey format

Each hwKey token encodes hardware and inference framework together, separated by an underscore (e.g. `b200_sglang`, `gb300_dynamo-sglang`). To find valid hwKey values, visit `/inference` on the live site, open the legend, and note the identifiers shown — or use **Export → Copy embed** to get a ready-made URL with your current filters already encoded.

### `i_metric` — accepted values

Full `y_*` internal keys (e.g. `y_tpPerGpu`, `y_costh`). The authoritative list is in `packages/app/src/lib/chart-utils.ts` (`Y_AXIS_METRICS`).

## Embed mode behavior

- Site header, footer, background decorations, and navigation are hidden on all `/embed/*` routes.
- A "SemiAnalysis InferenceX →" link appears in the chart caption (`Source: …`), deep-linking to the equivalent canonical dashboard URL. The canonical URL is built from the same embed params (minus `i_chart`), so opening it reproduces the same chart state on the main site.
- `robots: noindex, nofollow` is set on all embed routes — they won't appear in search results.
- An `embed_view` PostHog event is fired once on mount, capturing `referrer`, `embed_host`, `embed_chart`, `model` (`g_model`), `sequence` (`i_seq`), `precisions` (`i_prec`), `gpus` (from `i_active`), and `y_metric` (`i_metric`). This makes external embed traffic attributable in analytics.

## CSP / framing

Embed routes (`/embed/*`) set `Content-Security-Policy: frame-ancestors *`, allowing iframing from any origin.

All other routes set `frame-ancestors 'self'` and `X-Frame-Options: SAMEORIGIN`, blocking third-party framing.

## Recommended iframe snippet

```html
<iframe
src="https://inferencex.semianalysis.com/embed/scatter?g_model=DeepSeek-R1-0528&i_seq=8k%2F1k&i_prec=fp4&i_metric=y_tpPerGpu"
width="800"
height="500"
loading="lazy"
referrerpolicy="origin"
allow="clipboard-write"
style="border:none;border-radius:8px"
>
</iframe>
```

**Important — `referrerpolicy="origin"`:** many partner sites ship `<meta name="referrer" content="no-referrer">`. Without an explicit `referrerpolicy="origin"` on the `<iframe>`, the embed loses all referrer information, which breaks traffic attribution in the `embed_view` event. Use `origin` (not `strict-origin-when-cross-origin`) so the referrer is always sent.

`allow="clipboard-write"` is optional but needed if you want clipboard actions inside the embedded chart to work from the parent page.

You can copy the same ready-made iframe snippet from the dashboard: open the chart's **Export** menu and choose **Copy embed**.

For very short iframes (around 300–400 px tall), prefer `width` ≥ 1024 if you want the legend as a side column; below that width the legend uses a collapsible row at the bottom.
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ Design rationale and non-obvious conventions. See [CLAUDE.md](../CLAUDE.md) for
- [Data Transforms](./data-transforms.md) — Full pipeline from BenchmarkRow to RenderableGraph: type hierarchy, hardware key construction, derived metrics, memoization strategy
- [State Ownership](./state-ownership.md) — Which context owns which state, availability filtering cascade, comparison date mechanics, URL param sync
- [Blog](./blog.md) — MDX content system, SEO features (OG images, RSS, llms.txt, JSON-LD), TOC sidebar, reading progress, heading links, analytics events
- [Embed](./embed.md) — Stable iframe-embedding surface: `/embed/*` routes, URL contract, stability guarantee, CSP, recommended snippet
59 changes: 59 additions & 0 deletions packages/app/cypress/component/chart-buttons.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,65 @@ describe('ChartButtons', () => {
});
});

describe('embed scatter link', () => {
it('shows Copy embed when getEmbedUrl is provided and copies an iframe snippet to clipboard', () => {
const embedUrl = 'https://example.com/embed/scatter?model=dsr1';
cy.window().then((win) => {
cy.stub(win.navigator.clipboard, 'writeText').as('clipboardWrite');
});
cy.mount(
<div style={{ position: 'relative', width: 400, height: 200 }}>
<div id="test-chart">Chart content</div>
<ChartButtons
chartId="test-chart"
analyticsPrefix="test"
onExportCsv={cy.stub()}
getEmbedUrl={() => embedUrl}
/>
</div>,
);
cy.get('[data-testid="export-button"]').click();
cy.get('[data-testid="export-embed-button"]').should('be.visible').click();
cy.get('@clipboardWrite').should('have.been.calledOnce');
cy.get('@clipboardWrite')
.invoke('getCall', 0)
.its('args')
.its(0)
.should('include', embedUrl)
.and('include', '<iframe')
.and('include', 'referrerpolicy="origin"');
cy.get('[data-testid="export-embed-button"]').should('contain.text', 'Copied!');
});

it('shows the popover when only getEmbedUrl is provided', () => {
cy.window().then((win) => {
cy.stub(win.navigator.clipboard, 'writeText').as('clipboardWrite');
});
cy.mount(
<div style={{ position: 'relative', width: 400, height: 200 }}>
<div id="test-chart">Chart content</div>
<ChartButtons
chartId="test-chart"
analyticsPrefix="test"
getEmbedUrl={() => 'https://example.com/embed/scatter?model=dsr1'}
/>
</div>,
);
cy.get('[data-testid="export-button"]').click();
cy.get('[data-testid="export-png-button"]').should('be.visible');
cy.get('[data-testid="export-csv-button"]').should('not.exist');
cy.get('[data-testid="export-embed-button"]').should('be.visible');
cy.get('[data-testid="export-embed-button"]').click();
cy.get('@clipboardWrite').should('have.been.calledOnce');
cy.get('@clipboardWrite')
.invoke('getCall', 0)
.its('args')
.its(0)
.should('include', '<iframe')
.and('include', 'https://example.com/embed/scatter?model=dsr1');
});
});

describe('hideZoomReset', () => {
it('hides zoom reset button when hideZoomReset is true', () => {
cy.mount(
Expand Down
228 changes: 228 additions & 0 deletions packages/app/cypress/e2e/embed-scatter.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
function joinHeader(headers: Record<string, string | string[] | undefined>, name: string): string {
const pair = Object.entries(headers).find(([k]) => k.toLowerCase() === name.toLowerCase());
const v = pair?.[1];
if (Array.isArray(v)) return v.join(', ');
return typeof v === 'string' ? v : '';
}

describe('Embed — Scatter Chart', () => {
describe('default URL', () => {
before(() => {
cy.visit('/embed/scatter');
});

it('renders the embed root container', () => {
cy.get('[data-testid="embed-root"]').should('exist');
});

it('does not render the site header or footer', () => {
cy.get('[data-testid="header"]').should('not.exist');
cy.get('[data-testid="footer"]').should('not.exist');
});

it('renders an SVG chart with real data', () => {
cy.get('[data-testid="embed-scatter-figure"]', { timeout: 15000 }).should('exist');
cy.get('[data-testid="embed-scatter-figure"]').find('svg').should('exist');
cy.contains('No data available').should('not.exist');
});

it('shows the SemiAnalysis InferenceX attribution link', () => {
cy.get('[data-testid="embed-attribution"]')
.should('exist')
.should('contain.text', 'SemiAnalysis InferenceX');
});

it('attribution link points to the canonical /inference URL with seeded params', () => {
cy.get('[data-testid="embed-attribution"]')
.should('have.attr', 'href')
.and('include', '/inference?')
.and('include', 'g_model=DeepSeek-R1-0528')
.and('include', 'i_metric=y_tpPerGpu');
});

it('does not show the Shift+Scroll instructions text', () => {
cy.get('[data-testid="embed-chart-instructions"]').should('have.text', '');
});

it('has robots noindex meta tag', () => {
// Root layout may emit its own `meta[name="robots"]` first; embed routes add a noindex tag too.
cy.get('meta[name="robots"][content*="noindex"]').should('exist');
});
});

describe('custom params (site-style keys)', () => {
before(() => {
cy.visit('/embed/scatter?g_model=DeepSeek-R1-0528&i_seq=8k%2F1k&i_prec=fp4&i_metric=y_costh');
});

it('renders chart with the custom y metric', () => {
cy.get('[data-testid="embed-scatter-figure"]', { timeout: 15000 }).should('exist');
cy.contains('No data available').should('not.exist');
});

it('canonical link reflects the y metric override', () => {
cy.get('[data-testid="embed-attribution"]')
.should('have.attr', 'href')
.and('include', 'i_metric=y_costh');
});

it('canonical link does not include i_chart', () => {
cy.get('[data-testid="embed-attribution"]')
.should('have.attr', 'href')
.and('not.include', 'i_chart');
});
});

describe('i_active param restricts legend to creator-selected GPUs', () => {
before(() => {
cy.visit('/embed/scatter?i_active=b200_sglang');
});

it('renders chart without "No data available"', () => {
cy.get('[data-testid="embed-scatter-figure"]', { timeout: 15000 }).should('exist');
cy.get('[data-testid="embed-scatter-figure"]').find('svg').should('exist');
cy.contains('No data available').should('not.exist');
});

it('legend contains only the allowed GPU', () => {
// Each GPU renders as a <li> inside [data-testid="chart-legend"].
// With i_active=b200_sglang, only that GPU is in the allow-list, so
// exactly 1 <li> should appear (fp-indicators and controls use <div>).
cy.get('[data-testid="embed-scatter-figure"]', { timeout: 15000 }).should('exist');
cy.contains('No data available').should('not.exist');
cy.get('[data-testid="embed-legend-panel"] [data-testid="chart-legend"] li').should(
'have.length',
1,
);
});
});

describe('i_chart param selects chart variant', () => {
it('renders e2e chart by default', () => {
cy.visit('/embed/scatter');
cy.get('[data-testid="embed-scatter-figure"]', { timeout: 15000 }).should('exist');
});

it('renders interactivity chart when i_chart=interactivity', () => {
cy.visit('/embed/scatter?i_chart=interactivity');
cy.get('[data-testid="embed-scatter-figure"]', { timeout: 15000 }).should('exist');
cy.contains('No data available').should('not.exist');
});
});

describe('CSP headers', () => {
it('embed routes allow framing from any origin', () => {
cy.request('/embed/scatter').then((resp) => {
const csp = joinHeader(resp.headers, 'content-security-policy');
expect(csp).to.include('frame-ancestors *');
});
});

it('non-embed routes restrict framing to self', () => {
cy.request('/').then((resp) => {
const csp = joinHeader(resp.headers, 'content-security-policy');
expect(csp).to.include("frame-ancestors 'self'");
});
});
});

describe('viewport responsiveness', () => {
it('renders chart at 600×500 (narrow iframe)', () => {
cy.viewport(600, 500);
cy.visit('/embed/scatter');
cy.get('[data-testid="embed-scatter-figure"]', { timeout: 15000 }).should('exist');
cy.get('[data-testid="embed-scatter-figure"]').find('svg').should('exist');
cy.get('[data-testid="embed-legend-panel"]').should('be.visible');
});

it('legend dropdown stays inside the card at short height (800×400)', () => {
cy.viewport(800, 400);
cy.visit('/embed/scatter');
cy.get('[data-testid="embed-scatter-figure"]', { timeout: 15000 }).should('exist');
cy.get('[data-testid="embed-legend-panel"] summary').click();
cy.get('[data-testid="embed-legend-dropdown"]').should('be.visible');
cy.get('[data-testid="embed-scatter-figure"] [data-slot="card"]').then(($card) => {
cy.get('[data-testid="embed-legend-dropdown"]').then(($dd) => {
const cardTop = $card[0].getBoundingClientRect().top;
const ddTop = $dd[0].getBoundingClientRect().top;
expect(ddTop).to.be.at.least(cardTop);
});
});
});

it('renders chart at 800×600', () => {
cy.viewport(800, 600);
cy.visit('/embed/scatter');
cy.get('[data-testid="embed-scatter-figure"]', { timeout: 15000 }).should('exist');
cy.get('[data-testid="embed-scatter-figure"]').find('svg').should('exist');
cy.get('[data-testid="embed-legend-panel"]').should('be.visible');
});

it('fits short iframe height without document scroll (1024×420)', () => {
cy.viewport(1024, 420);
cy.visit('/embed/scatter');
cy.get('[data-testid="embed-scatter-figure"]', { timeout: 15000 }).should('exist');
cy.get('[data-testid="scatter-graph"] svg').should('exist');
cy.window().then((win) => {
expect(win.document.documentElement.scrollHeight).to.be.at.most(420);
});
cy.get('[data-testid="scatter-graph"] svg')
.invoke('attr', 'height')
.then((h) => {
const n = Number(h);
expect(n).to.be.at.least(240);
expect(n).to.be.below(600);
});
});

it('floors chart SVG height at 240px when iframe is very short (1024×250)', () => {
cy.viewport(1024, 250);
cy.visit('/embed/scatter');
cy.get('[data-testid="embed-scatter-figure"]', { timeout: 15000 }).should('exist');
cy.get('[data-testid="scatter-graph"] svg').invoke('attr', 'height').should('eq', '240');
cy.window().then((win) => {
expect(win.document.documentElement.scrollHeight).to.be.above(
win.document.documentElement.clientHeight,
);
});
});
});

describe('unofficial run overlay', () => {
it('renders overlay points when unofficialrun param is provided', () => {
cy.intercept('GET', '/api/unofficial-run*', {
statusCode: 200,
body: {
runInfos: [
{
id: 99999,
branch: 'test-branch',
url: 'https://github.com/test/repo/actions/runs/99999',
},
],
benchmarks: [
{
model: 'DeepSeek-R1-0528',
sequence: '8k/1k',
chart_type: 'e2e',
precision: 'fp4',
hw_type: 'b200_sglang',
tp: 8,
concurrency: 64,
ttft_ms: 300,
tpot_ms: 10,
e2e_latency_ms: 600,
total_throughput: 2000,
throughput_per_gpu: 250,
run_url: 'https://github.com/test/repo/actions/runs/99999',
},
],
},
}).as('unofficialRun');

cy.visit('/embed/scatter?unofficialrun=99999');
cy.get('[data-testid="embed-scatter-figure"]', { timeout: 15000 }).should('exist');
cy.get('[data-testid="embed-scatter-figure"]').find('svg').should('exist');
});
});
});
Loading