@jitto/analyze-embed
Guide: GUIDE.md, shipped in this package, takes you from nothing to an embedded dashboard.
Show a Jitto dashboard inside your own web app, for your own signed-in users, in React or plain JavaScript. Each user sees only the rows Jitto's row rules allow them; your page never holds a Jitto password or secret.
Status: 0.4. Expect small changes before 1.0. What changed in each version: CHANGELOG.md.
Not available yet:
filterChangedfires for the filters your app sets (source: 'host') only; changes the user makes in the filter bar are not reported yet. Everything else in this README works as described.
Filters need your Jitto server to list them. Pass
filters(or callsetFilters) only once your Jitto server returnsfilter_barin its token answer. Until then every filter is refused: a mount withfiltersends withfailedand the dashboard does not load. In 0.2.0 thefiltersoption was ignored instead.
Install
npm install @jitto/analyze-embed
One package, two entry points: @jitto/analyze-embed (plain JavaScript) and @jitto/analyze-embed/react (React).
Both run on the same core, so the same options give the same events. TypeScript declarations are included.
Quickstart
- Get three things from your Jitto tenant administrator: your tenant's Jitto host (for example
https://example-tenant.jittocloud.com;-uatand-devhosts for the other environments), the id of the dashboard, and confirmation that your website's origin (for examplehttps://portal.client.example) is on the tenant's allowed origins. Origins are exact:https://, host, optional port. - Install:
npm install @jitto/analyze-embed. - Write
fetchToken(see Tokens): it asks Jitto for a dashboard token with the signed-in user's own access token from your sign-in library. - Mount the dashboard:
import { JittoDashboard } from '@jitto/analyze-embed/react';
export function DeskOverview() {
return (
<JittoDashboard
dashboardId="3f0c6a52-8d1e-4b7a-9c2f-5e8d71a4b6c0"
embedOrigin="https://example-tenant.jittocloud.com"
fetchToken={fetchToken} // see Tokens: it sends the user's own access token
style={{ height: 600 }}
/>
);
}
Open the page as a test user. You see that user's rows.
React: <JittoDashboard>
import { useRef } from 'react';
import { JittoDashboard } from '@jitto/analyze-embed/react';
import type { JittoDashboardHandle } from '@jitto/analyze-embed';
export function Report() {
const dashboard = useRef<JittoDashboardHandle>(null);
return (
<JittoDashboard
ref={dashboard}
dashboardId="3f0c6a52-8d1e-4b7a-9c2f-5e8d71a4b6c0"
embedOrigin="https://example-tenant.jittocloud.com"
fetchToken={fetchToken}
theme={{ mode: 'light', colorPrimary: '#0b7a75' }}
onLoaded={({ durationMs }) => console.log(`loaded in ${durationMs} ms`)}
onFailed={({ reason, message }) => console.warn(reason, message)}
onSizeChanged={({ height }) => console.log('frame height', height)}
className="report"
/>
);
}
| Prop | Type | Required | What it does |
|---|---|---|---|
dashboardId | string | yes | The Jitto dashboard id your tenant administrator gave you. |
embedOrigin | string | yes | Your tenant's Jitto host, as an origin (https://example-tenant.jittocloud.com). The SDK frames only this origin followed by /embed (the API's embed_url) and talks only to this origin. It derives no host from anything else. |
fetchToken | (dashboardId, signal) => Promise<EmbedTokenResponse> | yes | Gets a dashboard token for the signed-in user with their own access token. See Tokens. |
filters | Filters | no | Filters applied before the first render, by filter name. A refused one ends the mount with failed. See Filters. |
theme | ThemeTokens | no | Colours, font, light or dark. See Theme. |
tokenDeadlineMs | number | no | Deadline for one token request. Default 20000. |
loadDeadlineMs | number | no | How long the frame may stay silent before failed fires with reason network. Default 30000. A mount that is not displayed, as in a hidden tab, waits until it is shown. |
retryDelaysMs | readonly number[] | no | Waits before each retry of a retryable token failure (429, 500, 502, deadline, network). Default [2000, 5000, 15000]. On mount, a Retry-After over 60 seconds is not waited for: failed fires instead. |
fetchDownload | (request, signal) => Promise<Response> | no | Deprecated and ignored: download() asks the dashboard, which downloads on its own session. Removed in the next minor release. |
downloadDeadlineMs | number | no | Deadline for one download(), including the dashboard's wait for a PDF. Default 180000. |
onLoaded | (e) => void | no | The dashboard rendered. See Events. |
onFailed | (e) => void | no | The dashboard could not load, or stopped working. |
onFilterChanged | (e) => void | no | The filters your app set were applied (source: 'host'). |
onFilterRejected | (e) => void | no | The dashboard refused filters from setFilters or a changed filters prop. Nothing was applied. For a prop change this is the only report. |
onSizeChanged | (e) => void | no | The dashboard frame's size on the page changed. See Size. |
className | string | no | Class of the container element. |
style | Record<string, string | number> | no | Style of the container element. Give it a height. |
ref | Ref<JittoDashboardHandle> | no | The same handle mountJittoDashboard returns. |
How prop changes apply:
- A change to
dashboardIdorembedOriginremounts the dashboard. themeis compared by value. An equal value does nothing, so an inline object is safe. A different value is applied in place, assetThemewould. Removingthemechanges nothing: a theme can be replaced, not removed, so the dashboard keeps its current look.filtersis compared by value too, key order ignored, sofilters={{ Ccy: userCcy }}is safe inline. A different value is applied assetFilterswould; a refusal callsonFilterRejectedand the filters in effect stay. Removingfilterschanges nothing; pass{}to clear every filter your app set. Use either the prop or the handle'ssetFiltersfor filters, not both.- Callbacks, the token function and the deadlines are read when used. A new function takes effect from its next use, with no remount.
The component mounts in the browser only. In a server-rendered app, render it on the client.
Plain JavaScript: mountJittoDashboard
import { mountJittoDashboard } from '@jitto/analyze-embed';
const element = document.getElementById('dashboard');
if (!element) throw new Error('no #dashboard element on the page');
const dashboard = mountJittoDashboard(element, {
dashboardId: '3f0c6a52-8d1e-4b7a-9c2f-5e8d71a4b6c0',
embedOrigin: 'https://example-tenant.jittocloud.com',
fetchToken,
onFailed: ({ reason, message }) => console.warn(reason, message),
});
const stop = dashboard.on('loaded', ({ durationMs }) => console.log(`loaded in ${durationMs} ms`));
// Later, when your app's own controls change:
export function useDarkTheme() {
dashboard.setTheme({ mode: 'dark' });
}
// When your page or view goes away:
export function teardown() {
stop();
dashboard.unmount();
}
mountJittoDashboard(element, options) takes the same options and callbacks as the React props (except className,
style and ref) and returns a handle:
| Member | What it does |
|---|---|
dashboardId | The dashboard id this mount shows. |
setFilters(filters) | Replaces the filters your app set. Returns a promise that resolves once the dashboard applied them; then filterChanged fires with source: 'host'. See Filters. |
setTheme(theme) | Replaces the theme tokens in place. The frame does not reload. |
download(format, options) | Downloads the dashboard as pdf, or one chart as csv (with chartId), exactly as the dashboard's own Download menu does, with the filters in effect in the dashboard. The SDK asks the dashboard, which downloads on its own session; your app sends no request. Rows are the user's own. Rejects with a DownloadError. See the guide's Add download buttons. |
on(event, handler) | Subscribes to an event. Returns a function that unsubscribes. |
unmount() | Removes the dashboard and cancels pending token requests and timers. Safe to call twice. |
The setFilters promise always settles. It rejects with a SetFiltersError whose outcome is:
deadline: the filters were not applied withinloadDeadlineMs.failedfires with reasonnetworkand the mount ends. A dashboard that is not displayed then (in a hidden tab) gets anotherloadDeadlineMsinstead.ended: the mount ended first, or had already ended.replaced: a latersetFilterscall came before this one was applied.
If the dashboard refuses a name or a value, setFilters rejects with a FilterError instead and applies nothing.
Its code is unknown_filter or invalid_filter_value, filterNames lists the refused names and
knownFilterNames lists every filter the dashboard has. filterRejected fires with the same error. A call made before
the first token arrives waits for it: the names are checked against the token's filter list then, in call order.
Tokens
Your page asks Jitto for a short-lived dashboard token with the signed-in user's own access token, the one your sign-in library already holds. That token is the user's identity: there is no token endpoint on your backend, no shared account and no secret, and your code never states who the user is or what they may see.
-
The SDK calls your
fetchToken(dashboardId, signal)on mount and again at 80 % of the token's lifetime (at most about 24.8 days later, the longest delay a browser timer holds). -
Your
fetchTokensends the user's own access token to Jitto, from the browser:POST /api/v1/embed-tokens HTTP/1.1 Host: example-tenant.jittocloud.com Origin: https://portal.client.example Authorization: Bearer <the user's own access token> Content-Type: application/json {"dashboard_id": "3f0c6a52-8d1e-4b7a-9c2f-5e8d71a4b6c0"} -
Jitto checks the token against your tenant's identity provider (issuer, signature, audience, expiry), reads the user's groups from it, and applies the row rules your tenant administrator stored in Jitto. It checks the
Originagainst your tenant's allowed origins, and answers the browser's CORS preflight only for those origins. -
Jitto answers with a dashboard token that lives 5 minutes. The answer has
Cache-Control: no-store:{ "token": "<opaque>", "dashboard_id": "3f0c6a52-8d1e-4b7a-9c2f-5e8d71a4b6c0", "embed_url": "https://example-tenant.jittocloud.com/embed", "expires_in": 300 } -
Your
fetchTokenhands it to the SDK with the SDK's field names (dashboardId,embedUrl,expiresIn, andfilterBarwhen the answer hasfilter_bar, the dashboard's filters), values unchanged. The SDK checks the answer itself and refuses a wrong one with reasontoken.
On an error Jitto answers with an HTTP status and a problem body (code, correlation_id), and with Retry-After
on a 429. A token that is expired, for another audience or from another issuer is refused with 401
unauthenticated (reason token); a user in no group the row rules read gets 403 no_group (reason permission).
Your tenant administrator registers your identity provider with Jitto once.
Your fetchToken rejects on an HTTP error with the status and the API's code. This exact code is tested against
401, 403, 422, 429, 502 and malformed answers. getAccessToken() is your sign-in library's:
async function fetchToken(dashboardId, signal) {
const r = await fetch('https://example-tenant.jittocloud.com/api/v1/embed-tokens', {
method: 'POST',
signal,
cache: 'no-store',
headers: {
Authorization: `Bearer ${await getAccessToken()}`, // the signed-in user's own token, from your sign-in library
'Content-Type': 'application/json',
},
body: JSON.stringify({ dashboard_id: dashboardId }), // nothing else: Jitto reads who the user is from the token
});
const body = await r.json().catch(() => null);
if (!r.ok) {
const retryAfter = Number(r.headers.get('Retry-After'));
throw Object.assign(new Error(`token request failed with HTTP ${r.status}`), {
status: r.status,
code: body?.code,
correlationId: body?.correlation_id,
retryAfterSeconds: retryAfter > 0 ? retryAfter : undefined,
});
}
if (!body || typeof body !== 'object') return body; // the SDK refuses it with reason `token`
const answer = { token: body.token, dashboardId: body.dashboard_id, embedUrl: body.embed_url };
if ('expires_in' in body) answer.expiresIn = body.expires_in;
if ('filter_bar' in body) answer.filterBar = body.filter_bar; // the dashboard's filters; without it, filters are refused
return answer; // the SDK checks it and refuses a wrong answer with reason `token`
}
Events
Subscribe with the on… props or callbacks, or with handle.on(name, handler).
| Event | Callback | Payload | When |
|---|---|---|---|
loaded | onLoaded | { dashboardId, durationMs } | The dashboard rendered for the first time after a mount. |
failed | onFailed | { dashboardId, reason, message, status?, code?, correlationId? } | The dashboard could not load, a token refresh failed for good, or a setFilters reload did not render in time. Fires at most once per mount; the mount is then finished and the SDK shows a short Jitto message in its place. Mount again to retry. |
filterChanged | onFilterChanged | { dashboardId, filters, source } | The filters in effect changed: once per applied setFilters call or filters prop change, with source: 'host'. 'user' (a change in the filter bar) is not reported yet. |
filterRejected | onFilterRejected | { dashboardId, error } | The dashboard refused filters from setFilters or a changed filters prop. error is the FilterError the call rejects with. Nothing was applied and the dashboard was not reloaded. |
sizeChanged | onSizeChanged | { dashboardId, width, height } | The dashboard frame's size on the page changed. See Size. |
reason is one of token, permission, network and not_found.
Size
Give the container a height, such as the window's height less your page's header. The dashboard frame takes the dashboard's own height within it:
- never taller than the container: a taller dashboard scrolls inside the frame, so its filter bar, messages and dialogs stay on screen;
- never shorter than the container or 600 px, whichever is less, so a short dashboard keeps room for its filter bar and dialogs. Below that, the frame ends where the dashboard does.
sizeChanged reports the frame's size on the page. You may size your container to it, so no space is left below a
short dashboard; that never makes the frame taller than the container was.
Filters
Filters set what the dashboard shows first, by the names its filter bar uses (for example Ccy,
Trade Date & Time), exactly, case included.
- Set them on mount with
filters, and later withsetFilters(or a newfiltersprop). - The names come with the token: Jitto's answer lists the dashboard's filters in
filter_bar, each anameand akind, and yourfetchTokenpasses it on asfilterBar. Without it every filter is refused. - Send a filter only to dashboards that have it. A name the dashboard lacks is refused with a
FilterError, and on mount the dashboard does not load; it is never ignored, so the dashboard never shows unfiltered data. - A value is a string, number or boolean, a list (several values), a date range
{ from, to }(two days writtenYYYY-MM-DD, such as2026-06-30,fromon or beforeto, both included), ornullto clear the filter. Which values a filter takes depends on itskind: a list of values on a multiple-choice filter, exactly two numbers[min, max]on a numeric range, a date range only on a date-range filter. - A list takes at most 1 000 values and a string at most 256 characters, the limits of the dashboard and of Jitto's
export API, so
download()is never refused for them. A value over them is refused withinvalid_filter_value, and so is a call that sets more than 50 filters. A filter whose name has more than 255 characters cannot be set. setFiltersreplaces every filter your app set: a name you leave out goes back to the dashboard's own default.- The filters travel in the dashboard's address, by name, which holds at most 4 096 bytes of them once encoded. A
call that needs more (a long list on a multiple-choice filter) is refused at once with
invalid_filter_value, naming the filters too long on their own (name and value), else its list-valued filters, else every filter. - Like any address, the dashboard's can appear in your Jitto host's access logs, filter names and values included. Pass the values a user picks (a currency, a date), never a secret or personal data.
download()gives the file with the filters in effect in the dashboard: those your app set, and the user's own filter-bar changes made since.- Filters narrow the view. They are not security. A user can change them in the filter bar. What a user may see is decided only by the row rules in Jitto.
- Today
setFiltersreloads the dashboard inside the frame. Your page stays mounted and no new token is requested, but the user's own filter-bar changes and scroll position are reset. Set filters on mount when you can.
Theme
Theme tokens change colours, font and light or dark. Jitto keeps the layout; there is no CSS hook.
| Token | What it changes |
|---|---|
mode | 'light' or 'dark'. |
colorPrimary | Buttons, selected states, links. |
colorBackground | The dashboard's page background. |
colorSurface | Chart and filter card background. |
colorText | Body text. |
colorBorder | Borders and dividers. |
fontFamily | A CSS font-family list. The font must already be loadable by the dashboard host. |
Colours are CSS colour strings (#1a73e8, rgb(…)). Anything else is ignored with a console warning. setTheme
never reloads the frame.
Errors and troubleshooting
Every failure arrives as one failed event with one of four reasons. status, code and correlationId come from
the API when it answered; quote the correlationId when you ask Jitto for help.
| Reason | Common causes | What to do |
|---|---|---|
token | The user's token was refused (401 unauthenticated: expired, wrong audience, wrong issuer); too many token requests (429 rate_limited, retried first); no token within tokenDeadlineMs; an answer for another dashboard or with an embedUrl that is not your embedOrigin followed by /embed. | Check the user is signed in and your sign-in library returns a current token. Check embedOrigin. Request a token once per page view and let the SDK refresh. |
permission | The dashboard is not on your tenant's embeddable list (403 not_embeddable); your origin is not allowed (403 origin_not_allowed); the user is in no group the row rules read (403 no_group); a dataset of the dashboard has no row rule (422 row_rules_incomplete); embedding is not switched on (503 embedding_not_configured). | Ask your tenant administrator to fix the setting the code names. |
not_found | No dashboard with this id (404 unknown_dashboard). | Check dashboardId. |
network | No answer, a server error (500, 502, retried first), a refused CORS preflight, or the frame stayed silent for loadDeadlineMs. A CORS error on the token request, or a frame-ancestors error, in the browser console means the page's origin is not allowed. | Check the network. For a CORS or frame-ancestors error, ask for your origin to be added exactly as the browser shows it (localhost and 127.0.0.1 differ, and so do ports). |
Other symptoms:
| You see | Cause | Fix |
|---|---|---|
| The dashboard loads with no rows | The user's groups match no row rule | Ask your tenant administrator which groups the row rules read. |
| A blank area with no error | The container has no height | Give the container (style or className) a height. |
| Your own CSP blocks the frame or the token request | Your frame-src or connect-src does not allow your tenant's Jitto host | Add your embedOrigin (for example https://example-tenant.jittocloud.com) to both frame-src and connect-src. |
Supported browsers and frameworks
Proposed for 1.0.0; the end-to-end tests confirm the list before release.
- Browsers: the current and previous major versions of Chrome, Edge, Firefox and Safari.
Pages must be served over
https://, including local development (for examplehttps://localhost:5173). - React: 18 and 19, through
@jitto/analyze-embed/react. - Everything else (Angular, Vue, server-rendered pages, no framework): use
mountJittoDashboardfrom@jitto/analyze-embed. Dedicated wrappers may follow on the same core. - TypeScript: declarations are included. Every option, event and payload is typed.
Versioning
- The package follows semantic versioning. A breaking change to an option, event, payload or failure reason is a new major version.
- The package's major version matches the embed API's path version:
@jitto/analyze-embed1.x talks to/api/v1/only. Additive API changes stay in v1. - A deprecated API version answers with
Deprecation,SunsetandLinkheaders for at least 90 days before it is removed.
Full guide and API reference
- The embedding guide, GUIDE.md in this package: the whole flow, the tenant administrator's settings, and a security checklist.
- The API reference, generated from the embed API's OpenAPI 3.1 contract. Your Jitto contact provides it with your tenant.
Both move to the Jitto docs site before 1.0.0, and this section then links there.