@apitraffic/embed-react

React bindings for the ApiTraffic embeddable request log. Two exports:
<RequestLog>: a drop-in component wrapping the @apitraffic/embed <apitraffic-request-log> Web Component (Shadow DOM, full CSS isolation), with token refresh wired to a callback.
useApiTrafficEmbed: a headless hook that fetches the same data and hands you plain state, for rendering your own UI. No Web Component, no Shadow DOM.
Both are driven by a short-lived embed token your server signs. The token, not your props, decides what the embed may show: the bucket, the filters, the request detail tabs and the features.
Contents
Requirements
- React and React DOM 16.8 or newer (peer dependencies; the package uses hooks).
@types/react is an optional peer, for TypeScript users.
- A plan that includes Embeds, and an Administrator API token with no access scopes on your server to mint embed tokens. The ingest token your SDK sends traffic with is refused.
- A server endpoint of your own that mints a token for the signed-in user (see Quick start).
- A browser with Custom Elements v1, Shadow DOM and ES2020 for
<RequestLog> (Chrome/Edge 80+, Firefox 74+, Safari 13.1+). See the @apitraffic/embed README.
- Node.js 20 or newer for tooling that imports the package (bundlers, SSR, tests).
Installation
npm install @apitraffic/embed-react
pnpm add @apitraffic/embed-react
yarn add @apitraffic/embed-react
@apitraffic/embed is a regular dependency and is installed with it. Add it to your own package.json too only if you import it directly (for its types, its headless entry, or the report and dashboard elements).
The package ships ESM and CommonJS builds with TypeScript declarations, resolved through its exports map, and is marked sideEffects: false. React and @apitraffic/embed are never bundled into it.
Quick start
1. Mint an embed token on your server
With an ApiTraffic Node SDK, embedToken() mints one in a single call. Set an Administrator API token with no access scopes as API_TRAFFIC_EMBED_TOKEN; the bucket, account and host come from the SDK you already configured.
const apitraffic = require('@apitraffic/node');
app.get('/api/embed-token', requireUser, async (req, res) => {
try {
const { token, expiresAt } = await apitraffic.embedToken({
filters: { tags: { customerId: req.user.customerId } },
ttl: 900,
tabs: ['request', 'response', 'connection', 'tags'],
features: { copyRequest: false, includeBodies: false }
});
res.json({ token, expiresAt });
} catch (error) {
console.error(error);
res.status(502).json({ error: 'Could not create an embed token' });
}
});
From any other language, POST https://api.apitraffic.io/v1/accounts/{accountSid}/embeds/token with that Administrator token as a bearer token. Every parameter is listed in the @apitraffic/embed README and the embed docs.
[!WARNING]
The token that mints embed tokens is a read credential to your captured traffic. An Administrator API token can mint embed tokens for any bucket in its account, with any filters or none, so whoever holds it can read all of the account's captured traffic through an embed. Keep it in a secret store (never in browser code or a repository), and rotate it immediately if it may have been exposed.
2. Render it
import { useEffect, useState } from 'react';
import { RequestLog } from '@apitraffic/embed-react';
async function fetchToken() {
const res = await fetch('/api/embed-token');
if (!res.ok) throw new Error(`token route answered ${res.status}`);
return (await res.json()).token;
}
export function ApiLog() {
const [token, setToken] = useState(null);
const [failed, setFailed] = useState(false);
useEffect(() => {
fetchToken().then(setToken, (error) => {
console.error(error);
setFailed(true);
});
}, []);
if (failed) return <p>The request log could not be loaded.</p>;
if (!token) return <p>Loading…</p>;
return (
<RequestLog
token={token}
theme="light"
pageSize={25}
onTokenExpired={fetchToken} // return the new token; the component applies it
onReady={(e) => console.log('Tabs:', e.detail.tabs)}
onError={(e) => console.error(e.detail.code, e.detail.message)}
onRequestClick={(e) => console.log('Opened', e.detail.requestSid)}
/>
);
}
<RequestLog>
A React wrapper around <apitraffic-request-log>. It renders the element, loads the Web Component with a dynamic import() in an effect (one load shared by every <RequestLog> on the page), hands the token over as a property, and bridges the element's events to callback props.
- The token stays out of the DOM. It is set on the element as a property, never rendered as an attribute, so it is in neither the server HTML nor the live DOM.
- One source of truth for the token. The
token prop sets it, and so does a string returned from onTokenExpired. A later token prop wins: a refresh that settles after the prop has changed is ignored.
Props
token | string | null | - | Required to show anything. The signed embed JWT. null clears the log. |
theme | 'light' | 'dark' | 'auto' | 'light' | 'auto' follows the OS via prefers-color-scheme. |
apiUrl | string | 'https://api.apitraffic.io/v1' | The API base URL (self-hosted or staging). |
pageSize | number | 25 | Requests per page. The API accepts 1 to 100. |
showDetail | boolean | true | false disables expanding a row into its detail. |
onTokenExpired | (event) => string | Promise<string> | void | - | Called about 30 s before the token expires, when it has, or when the API rejects it (event.detail.reason is 'expiring', 'expired' or 'unauthorized'). Return a new token to apply it. See Token refresh. |
onReady | (event) => void | - | After each successful load of the first page. event.detail is { tabs, features }. |
onError | (event) => void | - | Receives the CustomEvent; event.detail is { code, message }. See the codes below. |
onRequestClick | (event) => void | - | A row was expanded. event.detail.requestSid. |
className | string | - | CSS class on the host element. |
style | CSSProperties | - | Inline styles on the host element (custom properties included). |
There is no tabs or features prop: those come from the token, for the same reason filters do. Callback props can change on every render; the latest one is always called.
onError codes include the API's own error codes, plus:
TOKEN_REFRESH_FAILED | onTokenExpired threw, rejected, or did not settle within 30 s. |
TOKEN_EXPIRED | onTokenExpired returned a token that has already expired (usually the same, cached one). The log shows "Token refresh returned an expired token." |
EMBED_LOAD_FAILED | @apitraffic/embed could not be loaded. |
CLIPBOARD_UNAVAILABLE, CLIPBOARD_FAILED | Copy request could not use the clipboard. |
Token refresh
onTokenExpired is the whole refresh story: fetch a new token from your server and return it.
<RequestLog
token={token}
onTokenExpired={async () => {
const res = await fetch('/api/embed-token');
const { token: next } = await res.json();
return next;
}}
/>
- Never called concurrently. While one call is pending, further notices are ignored.
- Bounded. A call that has not settled within 30 s counts as failed (
TOKEN_REFRESH_FAILED), so a hung request cannot block later refreshes; a token it returns afterwards is ignored.
- Never a loop. The visitor's clock is corrected for only when the token's
iat proves it wrong; a clock that is somewhat fast just makes the callback fire that much early. While no load succeeds, repeated notices back off from 1 s up to 5 minutes.
- Returning nothing (or a non-string) leaves the token as it is. To replace it some other way, change the
token prop.
useApiTrafficEmbed
A headless hook that fetches embed data and returns raw state. It imports only @apitraffic/embed/headless, so no custom element is registered and no Shadow DOM is involved: you render everything.
import { useApiTrafficEmbed } from '@apitraffic/embed-react';
export function CustomRequestLog({ token }) {
const { requests, config, loading, loadingMore, error, hasMore, loadMore, refresh } =
useApiTrafficEmbed({
token,
pageSize: 50,
onTokenExpired: async () => {
const res = await fetch('/api/embed-token');
return (await res.json()).token;
}
});
if (loading) return <p>Loading…</p>;
if (error && requests.length === 0) return <p role="alert">{error}</p>;
return (
<div>
<button onClick={refresh}>Refresh</button>
<table>
<tbody>
{requests.map((req) => (
<tr key={req.sid}>
<td>{req.request?.method}</td>
<td>{req.request?.path}</td>
<td>{req.response?.statusCode}</td>
<td>{req.timings?.duration} ms</td>
</tr>
))}
</tbody>
</table>
{hasMore && (
<button onClick={loadMore} disabled={loadingMore}>
{loadingMore ? 'Loading…' : 'Load more'}
</button>
)}
</div>
);
}
Options
token | string | null | - | The signed embed JWT. Nothing loads without one; clearing it clears requests. |
apiUrl | string | 'https://api.apitraffic.io/v1' | The API base URL. |
pageSize | number | 25 | Requests per page (the API accepts 1 to 100). |
onTokenExpired | () => string | Promise<string> | void | - | Called about 30 s before the token expires, again at expiry if it was not replaced, and on a 401. Return a new token to apply it. Never called concurrently; repeated calls with no successful load in between back off from 1 s up to 5 minutes. Without it, an expired token sets error to "Embed token expired." |
Return value
requests | EmbedRequestRecord[] | The rows loaded so far. |
config | EmbedConfig | null | The token's resolved { view, theme, tabs, features }, once loaded. |
loading | boolean | true while the first page loads. |
loadingMore | boolean | true while loadMore() runs. |
error | string | null | The last error message, or null. |
hasMore | boolean | Whether another page is available. |
loadMore | () => Promise<void> | Fetch the next page and append it. |
refresh | () => void | Reload from the first page, keeping the current rows on screen until it answers. |
Guarantees
- No stale data under a new token. A new
token (including one returned by onTokenExpired), apiUrl or pageSize clears requests, config, hasMore, error and loadingMore in the very render that brings it (loading reads true there), so neither rows nor an error from one token are ever shown under another, even if the new load fails. Only refresh() keeps the current rows while it reloads.
- Cancellation. Every request is cancelled with an
AbortController when it stops being wanted (unmount, a new token, apiUrl or pageSize), a late answer for an old token is dropped, and no state is set after unmount.
- Late refreshes lose. A token
onTokenExpired returns is dropped if the token has changed since the refresh started, so a late answer for a previous customer never overrides a newer token prop.
- An expired token handed back is reported. If
onTokenExpired returns the same token and it has expired, error reads "Token refresh returned an expired token." A rejection, or no answer within 30 s, sets error to "Token refresh failed: …".
- Old
loadMore references are inert. A loadMore kept from an earlier render (an observer that captured it once) does nothing once its token, apiUrl or pageSize is no longer current. Use the one from the latest render.
Theming
<RequestLog> renders <apitraffic-request-log>, so it is themed exactly like the Web Component: the theme prop for light, dark or OS-following, and CSS custom properties on the element for everything else.
These values apply whatever the theme. To keep theme="dark" and theme="auto" readable, scope colour overrides to the light theme, as below (the default, so match "not dark or auto") or set dark values too; fonts, radius and height are safe to set unconditionally.
apitraffic-request-log:not([theme="dark"]):not([theme="auto"]) {
--at-font-family: 'Inter', sans-serif;
--at-bg-color: #ffffff;
--at-text-color: #1f2937;
--at-accent-color: #3b82f6;
--at-border-color: #e5e7eb;
--at-border-radius: 6px;
--at-row-hover-bg: #f9fafb;
--at-max-height: 600px;
}
Or per instance, through style or className:
<RequestLog token={token} style={{ '--at-max-height': '480px' }} />
The full list of custom properties and their defaults is in the @apitraffic/embed README. The headless hook renders nothing, so its look is entirely yours.
Tabs and features
Which request detail tabs the embed shows (request, response, connection, waf, related, workflows, tags, waterfall, traces, trace, concurrency, ai) and whether it offers Copy request or shows bodies is decided by the token, not by props. Set tabs and features when minting it. The API withholds the data behind every tab you leave out, so there is nothing for a viewer to un-hide in the DOM, and nothing for the headless hook to receive either.
onReady receives the resolved list as event.detail.tabs and event.detail.features; the hook exposes it as config. The tab descriptions, the default set and the tabs that reach beyond the request being viewed are documented in the @apitraffic/embed README.
Reports and dashboards in React
This package has no wrapper for <apitraffic-report> and <apitraffic-dashboard>. Render the elements from @apitraffic/embed directly, load the package in an effect, and set the token through a ref so it never becomes an attribute:
import { useEffect, useRef } from 'react';
export function UsageDashboard({ token }) {
const ref = useRef(null);
useEffect(() => {
import('@apitraffic/embed');
}, []);
useEffect(() => {
if (ref.current) ref.current.token = token;
}, [token]);
return <apitraffic-dashboard ref={ref} frame-title="Your API usage" />;
}
Listen for apitraffic:token-expired on ref.current with addEventListener to refresh the token. See Reports and dashboards for the token parameters, attributes and events. In TypeScript, declare the tag in JSX.IntrinsicElements yourself.
Server rendering and React Server Components
- Both exports are client components. The built files start with
'use client', so Next.js and other RSC frameworks treat them as client boundaries. Import them from a Server Component and pass the token as a prop.
- Importing on a server is safe.
<RequestLog> renders the bare element on the server (with its theme, page-size and show-detail attributes, never the token) and loads the Web Component with a dynamic import() in an effect, so nothing browser-only runs during SSR. The CommonJS build turns that import into a deferred require(), so CommonJS consumers (Jest, older SSR setups) never meet a bare import().
- The hook only fetches in effects, so it renders
loading: true on the server when it has a token and loads in the browser.
TypeScript
Declarations ship with the package for import and require.
import { RequestLog, useApiTrafficEmbed } from '@apitraffic/embed-react';
import type { RequestLogProps, UseApiTrafficEmbedOptions, UseApiTrafficEmbedResult } from '@apitraffic/embed-react';
RequestLogProps | <RequestLog>'s props. The callbacks receive CustomEvents typed with @apitraffic/embed's RequestLogTokenExpiredDetail, RequestLogReadyDetail, RequestLogErrorDetail and RequestLogRequestClickDetail. |
UseApiTrafficEmbedOptions | The hook's options. |
UseApiTrafficEmbedResult | The hook's return value. requests is EmbedRequestRecord[] and config is EmbedConfig | null, both from @apitraffic/embed. |
Import record and event types (EmbedRequestRecord, EmbedConfig, EmbedTab, …) from @apitraffic/embed.
Security model
- Signed server-side. Embed JWTs are signed with a per-account HMAC-SHA256 secret, and filters are locked into the token: the client cannot modify them.
- Tabs are the access boundary. Exposed tabs and features are locked into the token too, and enforced server-side: responses are rebuilt from only the fields the granted tabs need, and the per-tab endpoints answer as if they do not exist unless their tab is granted.
- Short-lived. Tokens last at most 24 hours (default one hour), with refresh built in.
- Kept out of the DOM.
<RequestLog> sets the token as a property, so it is in neither the server-rendered HTML nor the live DOM, where session-replay tools and other scripts could read it.
- Embed endpoints accept only embed JWTs. Standard API tokens and session tokens are rejected.
- Your minting route is your responsibility. Require your user's sign-in, decide
filters, tabs, features and ttl on the server from that user, and keep ttl short.
Troubleshooting
Nothing renders, and onError reports EMBED_LOAD_FAILED. @apitraffic/embed could not be imported. Check that it is installed (it is a dependency of this package) and that your bundler resolves it.
onError reports TOKEN_REFRESH_FAILED. onTokenExpired threw, rejected or took longer than 30 s. Check your token route; event.detail.message carries the reason.
"Token refresh returned an expired token." onTokenExpired returned a token that has already expired, usually a cached one. Mint a new token on every call.
The log is empty. Check that the token's bucket has traffic and its filters are not too narrow. With the hook, check error and that token is set.
A tab or body is missing. The token decides: read event.detail.tabs in onReady (or config.tabs from the hook). Bodies need features.includeBodies: true on the token.
The token request is refused with 403. Embed tokens need an Administrator API token with no access scopes (403, code 2010, otherwise; for a scoped token the message says its access scopes do not allow the action). Code 2221 means your plan does not include Embeds.
More in the embed docs.
Documentation and related packages
Changelog
Release history is on npm.
License
MIT
Follow us on our Blog and on X.
Chat with the ApiTraffic team and other members on Discord and follow our tutorials and other videos on YouTube.


