Source maps

A minified stack trace says a.b is not a function at app-D4wpJuT4.js:1:48213. Source maps turn that back into your file, your line, your function and the code around it.

Your build uploads its maps to LaraBug, tagged with a release. The SDK sends the same release with every error. LaraBug maps each frame with the maps of that release, so the maps never have to be public.

Vite

Install the plugin:

Terminal
npm install --save-dev @larabug/vite

Add it to vite.config.js, next to laravel-vite-plugin in a Laravel app:

vite.config.js
import { defineConfig } from 'vite'
import laravel from 'laravel-vite-plugin'
import larabug from '@larabug/vite'

export default defineConfig({
  plugins: [
    laravel({ input: ['resources/js/app.js'] }),
    larabug(),
  ],
})

Give the build your application's ingest key, or its DSN, from the application settings:

.env
LARABUG_KEY=lbi_your_ingest_key

On vite build the plugin:

  1. Picks a release: LARABUG_RELEASE when set, otherwise the commit being built.
  2. Writes hidden source maps, so the scripts carry no sourceMappingURL comment.
  3. Puts the release in your bundle, where @larabug/browser picks it up for every error.
  4. Uploads every map with the url of its script, then deletes the maps from the output.

vite dev is left alone. A failed upload never fails the build unless you pass failOnError: true.

Webpack and Laravel Mix

Two things have to line up: the bundle has to carry the release, and the maps have to be uploaded under that same release. DefinePlugin sets the release, and hidden-source-map writes the maps without pointing browsers at them:

webpack.config.js
const { execSync } = require('child_process')
const webpack = require('webpack')

const release = process.env.LARABUG_RELEASE || execSync('git rev-parse HEAD').toString().trim()

module.exports = {
  mode: 'production',
  devtool: 'hidden-source-map',
  plugins: [new webpack.DefinePlugin({ __LARABUG_RELEASE__: JSON.stringify(release) })],
}

Laravel Mix takes the same settings through webpackConfig:

webpack.mix.js
const { execSync } = require('child_process')

const release = process.env.LARABUG_RELEASE || execSync('git rev-parse HEAD').toString().trim()

mix.js('resources/js/app.js', 'public/js')
  .webpackConfig((webpack) => ({
    devtool: 'hidden-source-map',
    plugins: [new webpack.DefinePlugin({ __LARABUG_RELEASE__: JSON.stringify(release) })],
  }))

Then upload after the build, with the same release, and remove the maps so they are not served:

Terminal
npx @larabug/sourcemaps upload --release "$(git rev-parse HEAD)" --dir public/js --url-prefix "~/js" --remove-source-maps

The uploader reads the key from LARABUG_KEY the same way the Vite plugin does. npx @larabug/sourcemaps list --release <release> prints what LaraBug holds for a release.

Matching files

Every map is uploaded with the url of the script it belongs to, and a frame uses the map whose url matches its file:

  • A path starting with ~/, such as ~/build/assets/app-D4wpJuT4.js, matches that path on any host. One upload covers production, staging and every preview domain.
  • A full url, such as https://cdn.example.com/build/assets/app-D4wpJuT4.js, matches that host only.

A query string on the script, such as a cache buster, is ignored.

Naming the build yourself

Without a bundler plugin, set release when you start the client, and upload with the same string:

JavaScript
init({
  dsn: import.meta.env.VITE_LARABUG_DSN,
  release: import.meta.env.VITE_APP_VERSION,
})

An error without a release cannot be matched to any map, and the issue page says so.

On the issue page

A JavaScript error mapped to its source shows the original file, line and function of each frame, with the code around it when the build included sourcesContent, as Vite and webpack do by default. A toggle shows the minified frames the browser reported. Frames from scripts with no map, such as a third party widget, stay minified and are marked as such.

Issues group on the original line. An error that was already grouped before you uploaded maps stays in the issue it was in.

Maps uploaded after an error arrived still apply: the errors of that release are filled in once the upload lands.

Limits and retention

  • One map can be up to 50 MB, and one release can hold up to 1000 maps.
  • LaraBug keeps the maps of your newest 10 releases, counted by when they were last uploaded. Errors keep their original frames after the maps are gone.
  • Uploading the same file for the same release again replaces it.

The application settings list every release and its files under Source maps, where you can delete a file or a whole release.

The upload API

The plugin and the uploader call two endpoints, which you can call from any tool:

Terminal
curl -X POST https://www.larabug.com/api/sourcemaps \
  -H "X-LaraBug-Ingest-Key: lbi_your_ingest_key" \
  -F release=v1.4.0 \
  -F minified_url="~/build/assets/app-D4wpJuT4.js" \
  -F sourcemap=@public/build/assets/app-D4wpJuT4.js.map

It answers 201 for a new file, 200 for a replaced one, 413 for a map over the size limit, 422 for a file that is not a version 3 source map, and 401 or 403 for credentials it does not accept. GET /api/sourcemaps/releases/{release} lists the files a release has.