๐Ÿงช Insights

Stage: prototyping

Insights allow your application to collect real user usage information to optimize the creation of bundles. By observing real user behavior, the Qwik build system can then do a better job prefetching bundles for your application. There are two benefits of this:

  1. By noticing which symbols are used together, the bundler can colocate the symbols in the same bundle minimizing the waterfall that could occur if there are too many small files needing to be downloaded.
  2. By observing in which order the symbols are used, the prefetcher can then fetch bundles in priority order ensuring that the bundles that are used more often are loaded first.

Architecture

The optimization consists of these parts:

  • A <Insights> component which collects time based bundle execution data.
  • A qwikInsights Vite Plugin to apply the usage data during the build process.
  • A registered application inside the builder.io database.
NOTECurrently Insights info is hosted in the Builder database. This information is about the execution of symbols/chunks in the application. The implementation of the service is open-source and you have the choice to use ours or host your own.

<Insights> component

The <Insights> component should be added to your root.tsx file.

To use it, you need to pass experimental: ['insights'] as an option to the qwikVite plugin as such:

// vite.config.ts
 
export default defineConfig(({ command, mode }): UserConfig => {
  return {
    plugins: [
      // ... other plugins like qwikRouter() etc
      qwikVite({
        experimental: ['insights']
        // ... other options
      }),
 
    ],
    // ... rest of the config
  };
}
 
// ...
import { Insights } from '@qwik.dev/core/insights';
 
export default component$(() => {
  useQwikRouter();
 
  // ...
  return (
    <>
      <head>
        // ...
        <Insights />
      </head>
      <body lang="en">
        // ...
      </body>
    </>
  );
});

The component reads its configuration from the qwikInsights Vite plugin (see below), so it takes no props. You can get the PUBLIC_QWIK_INSIGHTS_KEY the plugin needs by visiting Qwik Insight.

The <Insights> component collects this data:

  • Timing information of symbols.
  • The route the symbols were used on.
  • Uncaught JavaScript errors.
NOTEThe <Insights> component does not collect any user-identifiable information.

The data collected being based on the frequency bundles are being grouped together at the same time, it is not advised to share the same API key between your preview and production environments. During preview, developers might use the application in 3G throttling and click around, which can put unrelated bundles together. After that the import graph would be wrong, leading to over-preloading, and Insights would then base its future data collection based on those corrupted bundles. If that happened to you, we recommend creating a new API key for your production environment. In normal usage, you will release a new version of your application and there will be a long enough window for Insights to collect the data. In case you face over-preloading in your app, you can always re-create a new API key. Drop us a message in the Qwik Discord server if this becomes a recurrent issue for you.

Vite integration

Once the application is running for a while and it collects sufficient information on symbol usage, the stats can be used to improve the bundles of the future version of the application. This is done by connecting the vite build with Insights like so:

file: vite.config.js

//..
import { defineConfig, loadEnv } from 'vite';
import { qwikInsights } from '@qwik.dev/core/insights/vite';
 
export default defineConfig(async () => {
  return {
    plugins: [
      qwikInsights({
        publicApiKey: loadEnv('', '.', '').PUBLIC_QWIK_INSIGHTS_KEY,
      }),
      //...
    ],
    // ...
  };
});

Sampling

Only a fraction of sessions report: 5% in a production build, every session in dev. A small inline script decides once per browser tab session, and only sampled sessions download the tracker. Set sampleRate (from 0 to 1) to change it:

qwikInsights({
  publicApiKey: loadEnv('', '.', '').PUBLIC_QWIK_INSIGHTS_KEY,
  sampleRate: 0.2, // 20% of sessions
}),
NOTEBefore sampling was added, every session reported. Set sampleRate: 1 to keep that behavior.

Self-hosted collection

Instead of the central Insights service, you can collect sessions into your own libSQL database: a local SQLite file, or a remote one such as Turso or a self-hosted libSQL server. Three pieces work together:

  1. The tracker in sampled browser sessions posts beacons to postUrl.
  2. A collector route in your app receives them and writes them into the database.
  3. The build reads the database and computes the bundling strategy from it.

Which setup fits depends on where your app runs. See Choosing a setup for complete examples.

Settings

SettingWhereWhat it does
publicApiKeyqwikInsights()Names your app in the database. When self-hosting, any name of 5 or more characters works.
postUrlqwikInsights()Where browsers send beacons: your collector route, on the same origin as the site.
dbqwikInsights()The database the build reads. Relative file paths resolve against the Vite root. Defaults to QWIK_INSIGHTS_DB_URL.
QWIK_INSIGHTS_DB_URLenvironmentThe database for the build and the collector, e.g. file:/var/lib/app/insights.db or libsql://my-db.turso.io. The collector defaults to file:.qwik/insights.db.
QWIK_INSIGHTS_DB_AUTH_TOKENenvironmentThe auth token for a remote database.
sampleRateqwikInsights()The fraction of sessions that report; see Sampling.

When the build has a database but no postUrl, it leaves the tracker out, so beacons never reach the central service by accident.

The collector route

Record the beacons in a Qwik Router endpoint. Client errors are posted to postUrl + error/, so use a catch-all route that handles both:

// src/routes/insights-collect/[...path]/index.ts
import { handleInsightsBeacon } from '@qwik.dev/core/insights/collector';
import type { RequestHandler } from '@qwik.dev/router';
 
export const onPost: RequestHandler = async ({ request, json }) => {
  json(200, {
    ok: await handleInsightsBeacon(request, {
      publicApiKey: import.meta.env.PUBLIC_QWIK_INSIGHTS_KEY,
    }),
  });
};

handleInsightsBeacon() validates each beacon, records it, and never throws, so a broken beacon can't fail the response. Passing publicApiKey makes the endpoint ignore beacons from other apps. It reads the database from QWIK_INSIGHTS_DB_URL, or from the dbUrl and authToken options.

NOTEThe collector route must be on the same origin as your pages: Qwik Router rejects cross-origin beacons in its CSRF check. The endpoint is public, so consider rate limiting it at your proxy or platform.

In dev you don't need the route. Without postUrl, the dev server records beacons into db itself, and you can inspect them in the devtools Insights tab.

NOTELocal database files need Node: its built-in node:sqlite (Node 22.5+), or the optional @libsql/client package on older Node. Remote databases always use @libsql/client, which also runs on edge runtimes.

Choosing a setup

If you don't want to run anything yourself, use the central service as described in Vite integration. It works with any deployment, including static sites. The setups below keep the data in your own database.

Solo developer, static site

Your site is built to static files (for example with the static site adapter) and served from a static host. There is no server to receive beacons, so collect your own sessions while you develop, and build from those.

// vite.config.ts
qwikInsights({
  publicApiKey: 'my-site',
  db: 'file:.qwik/insights.db',
}),
  1. Run the dev server and click through the flows that matter. Dev records every session. Running your end-to-end tests against the dev server records their sessions too.
  2. Run vite build locally. It computes the strategy and writes it to q-insights.json in the project root.
  3. Commit q-insights.json, and keep .qwik/ out of git. Your CI has no database, so its build uses the committed q-insights.json instead.

The built pages ship without the tracker, and the build prints a warning saying so. The strategy reflects the flows you exercised rather than real visitors, so refresh it whenever your app's main flows change.

Team with its own server

You deploy to a Node server you control (for example with the Node adapter, or in a container). The server runs the collector route, and the build reads the same database.

// vite.config.ts
qwikInsights({
  publicApiKey: loadEnv('', '.', '').PUBLIC_QWIK_INSIGHTS_KEY, // e.g. 'acme-shop'
  postUrl: '/insights-collect/',
}),

Point QWIK_INSIGHTS_DB_URL at the database on the server, and wherever you build:

  • One server that also builds: use a file outside the deploy directory so redeploys keep it, e.g. QWIK_INSIGHTS_DB_URL=file:/var/lib/acme/insights.db.
  • Builds in CI, or several server instances: use one database they can all reach, such as a self-hosted libSQL server or Turso. Set QWIK_INSIGHTS_DB_URL=libsql://โ€ฆ and QWIK_INSIGHTS_DB_AUTH_TOKEN on the servers and in CI, and install @libsql/client. A separate file per instance would only see part of the traffic.

In dev, the same route records into a local .qwik/insights.db unless you set the variable, so your own clicking doesn't end up in the production data.

Serverless and edge, with Turso

You deploy to Vercel, Netlify or Cloudflare (Qwik's edge adapters), or to a serverless Node platform such as AWS Lambda. There is no disk that survives between requests, so the collector writes to a remote libSQL database such as Turso.

  1. Create the database and a token with the Turso CLI:

    turso db create qwik-insights
    turso db show qwik-insights --url
    turso db tokens create qwik-insights
  2. Install the client: npm install @libsql/client.

  3. Set QWIK_INSIGHTS_DB_URL and QWIK_INSIGHTS_DB_AUTH_TOKEN in your platform's environment settings and in your CI, so both the collector and the build can reach the database.

  4. Add the collector route. Edge runtimes don't all have process.env, so read the settings from the request's env, which works on every adapter:

    // src/routes/insights-collect/[...path]/index.ts
    import { handleInsightsBeacon } from '@qwik.dev/core/insights/collector';
    import type { RequestHandler } from '@qwik.dev/router';
     
    export const onPost: RequestHandler = async ({ request, env, json }) => {
      json(200, {
        ok: await handleInsightsBeacon(request, {
          dbUrl: env.get('QWIK_INSIGHTS_DB_URL'),
          authToken: env.get('QWIK_INSIGHTS_DB_AUTH_TOKEN'),
          publicApiKey: import.meta.env.PUBLIC_QWIK_INSIGHTS_KEY,
        }),
      });
    };
  5. Configure the plugin with postUrl. The build reads the database from the environment:

    // vite.config.ts
    qwikInsights({
      publicApiKey: loadEnv('', '.', '').PUBLIC_QWIK_INSIGHTS_KEY,
      postUrl: '/insights-collect/',
    }),

Preview and production environments

Preview deployments get traffic that doesn't look like real usage, such as testers clicking around on throttled connections. Keep that out of the production strategy. Give each environment its own publicApiKey (for example acme-shop and acme-shop-preview), or its own database. The collector stores the key with every beacon, and the build only reads data for its own key.

Contributors

Thanks to all the contributors who have helped make this documentation better!

  • mhevery
  • gioboa
  • mrhoodz
  • thejackshelton
  • maiieul