Skip to content
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