@autter/runtime-cli 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,27 +2,95 @@
2
2
 
3
3
  Readable stack traces for Autter Runtime. Gives every JS bundle a
4
4
  content-addressed **debug id** and uploads its **source map** to the ingester,
5
- so minified browser frames resolve to your original source, even when the
5
+ so minified browser frames and bundled Node and edge frames resolve to your
6
+ original source, with the lines around each frame of your code, even when the
6
7
  release your SDK reports doesn't match the upload.
7
8
 
8
- - `autter-runtime sourcemaps inject <dir>` / `upload <dir>` CLI
9
- - Vite/Rollup plugin: `@autter/runtime-cli/vite`
10
- - webpack 5 plugin: `@autter/runtime-cli/webpack`
9
+ - `autter-runtime sourcemaps setup` wires it into a project; `verify` checks a build
10
+ - `autter-runtime sourcemaps inject <dir>` / `upload <dir>` CLI, for browser
11
+ and server output alike (tsup, esbuild, a Worker)
12
+ - Vite/Rollup plugin: `@autter/runtime-cli/vite` (SSR builds and server
13
+ environments too)
14
+ - webpack 5 plugin: `@autter/runtime-cli/webpack` (Node targets too)
11
15
  - Next.js wrapper: `withAutterSourceMaps` from `@autter/runtime-cli/next`
16
+ (client and `.next/server` output)
12
17
 
13
- No runtime dependencies; bundlers are optional peer dependencies. Debug ids
14
- need ingester 1.6.0+ and `@autter/runtime-browser` 1.6.0+ (older ingesters
15
- still match `.js` maps by release + path). Full guide:
16
- [docs/SOURCE-MAPS.md](../../docs/SOURCE-MAPS.md).
18
+ Bundlers are optional peer dependencies. Debug ids need ingester 1.6.0+ and
19
+ `@autter/runtime-browser` 1.6.0+ (older ingesters still match `.js` maps by
20
+ release + path). Server bundles and source contents need the current
21
+ ingester. Full guide: [docs/SOURCE-MAPS.md](../../docs/SOURCE-MAPS.md).
17
22
 
18
- ## Install
23
+ ## Quick start (people and coding agents)
19
24
 
20
25
  ```bash
21
- npm install -D @autter/runtime-cli
26
+ npx @autter/runtime-cli@latest sourcemaps setup # in the app's directory
27
+ npm run build # with AUTTER_RUNTIME_KEY set
28
+ npx autter-runtime sourcemaps verify dist # before deploying
22
29
  ```
23
30
 
24
- Set `AUTTER_RUNTIME_KEY` to a **server** ingest key (`autter_rt_…`) where your
25
- build runs. Without it, the plugins turn no source maps on and change nothing.
31
+ **`setup [dir]`** is non-interactive and safe to re-run. It detects the build
32
+ tool (Vite and the frameworks built on it: SvelteKit, Nuxt, Astro, Remix,
33
+ React Router, Solid, Qwik; Next.js; webpack and Vue CLI; Rollup; Create React
34
+ App; Angular CLI; Parcel; esbuild), the package manager, the CI (GitHub
35
+ Actions, Vercel, Netlify, Cloudflare Pages, GitLab CI), the output directory,
36
+ existing source map settings, the installed browser SDK, and whether
37
+ `initAutterBrowser` gets a `release`. Then it:
38
+
39
+ - adds `@autter/runtime-cli` to `devDependencies` and prints the install
40
+ command (`--install` runs it);
41
+ - adds the Vite/Rollup or webpack plugin, or wraps the Next.js config in
42
+ `withAutterSourceMaps` (outermost), when the config has a shape it can edit
43
+ safely: `export default defineConfig({...})`, `module.exports = {...}`, a
44
+ config variable, a function returning an object, `withX(nextConfig)`
45
+ wrappers. It parses the file (JavaScript or TypeScript) and changes only
46
+ what it adds; anything else becomes a `manual` action with the exact snippet;
47
+ - for builds without a plugin (CRA, Angular, Parcel, esbuild), adds a
48
+ `postbuild` step (appended to `build` for pnpm, Yarn 2+ and Bun, which skip
49
+ `postbuild`) that runs `inject`, then `upload --delete-after-upload
50
+ --best-effort`, and turns on hidden maps where the tool can
51
+ (`GENERATE_SOURCEMAP=true`, Angular `"sourceMap": { "scripts": true, "hidden": true }`);
52
+ - reports, never edits, app code: a missing `release` gets a `manual` action
53
+ showing how to pass the commit SHA through the tool's public env or define
54
+ mechanism;
55
+ - never writes a key. It says where to store `AUTTER_RUNTIME_KEY` for each CI
56
+ it found, with commands that read the value from a prompt or stdin, never
57
+ from argv: `gh secret set AUTTER_RUNTIME_KEY --repo <owner/repo>`,
58
+ `vercel env add AUTTER_RUNTIME_KEY production`, `netlify env:set …
59
+ "$(cat)"`, and dashboard notes for Cloudflare Pages and GitLab.
60
+
61
+ `--dry-run` writes nothing; `--json` prints `{ detected, actions: [{ type:
62
+ "edit"|"add-dependency"|"add-script"|"manual"|"set-secret", file?,
63
+ description, snippet?, command? }], secret: { name, key, where }, next }`.
64
+ Exit codes: `0` everything applied, `2` manual actions remain, `1` error.
65
+
66
+ **`verify <dir>`** checks a production build before deploy: every JS bundle
67
+ has a debug id (the comment and the registration snippet), no `.map` file is
68
+ left and no `sourceMappingURL` points at a missing map (both deploy risks),
69
+ and, when `AUTTER_RUNTIME_KEY` is set, every bundle's map is on the ingester
70
+ (`/v1/sourcemaps/check` by debug id, ingester 1.7.0+). Without a key the
71
+ local checks still run and the upload is reported as unverified. Files copied
72
+ unchanged from `public/`, Next.js build manifests and polyfills, and service workers are
73
+ skipped; `--ignore <glob>` skips more. `--allow-maps` reports leftover maps
74
+ without failing. `--server` checks server output (a tsup or esbuild `dist/`,
75
+ `.next/server`), where leftover maps are a note, not a failure, and Next.js
76
+ server manifests are skipped. `--json` for machines; exit `1` on any failure,
77
+ with the reason per file.
78
+
79
+ ## Keys
80
+
81
+ Set `AUTTER_RUNTIME_KEY` where your build runs. The recommended key for CI is
82
+ an **upload-only key** (`autter_rtu_…`): it can upload and check source maps
83
+ and nothing else, so a leaked CI secret can't send telemetry. The full server
84
+ key (`autter_rt_…`) also works. A publishable client key (`autter_rtc_…`) is
85
+ refused. Upload keys need ingester 1.7.0+. Without a key, the plugins turn no
86
+ source maps on and change nothing, and `upload --best-effort` uploads nothing
87
+ and exits 0.
88
+
89
+ ## By hand
90
+
91
+ ```bash
92
+ npm install -D @autter/runtime-cli
93
+ ```
26
94
 
27
95
  ## Vite / Rollup
28
96
 
@@ -53,10 +121,20 @@ All three turn on hidden source maps when you haven't configured maps, inject
53
121
  debug ids after the build, upload when a key is present, and then delete the
54
122
  maps they created whether or not the upload succeeded (a rejected upload, an
55
123
  unreachable ingester, a dry run), printing one line that says why; without a
56
- key they turn none on. Maps you configured yourself are left alone. Options (all optional): `release`, `urlPrefix`,
57
- `endpoint`, `key`, `upload: false`, `deleteAfterUpload`, `dryRun`,
58
- `concurrency`, `failOnError` (default `false`: a failed upload is logged and
59
- never fails the build), `silent`.
124
+ key they turn none on. Maps you configured yourself are left alone.
125
+
126
+ Server bundles get the same: Vite SSR builds and Vite 6 server environments,
127
+ webpack builds for a Node target, and Next.js `.next/server` (Node and edge,
128
+ webpack and Turbopack). Their maps are stored under the output directory's
129
+ path in the project (`/dist/server/`, `/.next/server/`), which the ingester
130
+ matches against the path the server runs them from, never under the public
131
+ URL prefix.
132
+
133
+ Options (all optional): `release`, `urlPrefix`, `endpoint`, `key`,
134
+ `upload: false`, `deleteAfterUpload`, `dryRun`, `concurrency`, `failOnError`
135
+ (default `false`: a failed upload is logged and never fails the build),
136
+ `silent`, `sourcesContent: false` (upload maps without your files' source),
137
+ `serverBundles: false` (leave server output exactly as the bundler wrote it).
60
138
 
61
139
  ## CLI
62
140
 
@@ -64,35 +142,58 @@ never fails the build), `silent`.
64
142
  npx autter-runtime sourcemaps inject dist
65
143
  npx autter-runtime sourcemaps upload dist --url-prefix / --release "$GIT_SHA"
66
144
  npx autter-runtime sourcemaps upload dist --dry-run
145
+
146
+ # A bundled Node server (tsup, esbuild): the same commands
147
+ npx autter-runtime sourcemaps inject dist
148
+ npx autter-runtime sourcemaps upload dist --url-prefix /dist/
149
+ npx autter-runtime sourcemaps verify dist --server
67
150
  ```
68
151
 
69
152
  ```
70
- autter-runtime sourcemaps inject <dir> [--dry-run]
153
+ autter-runtime sourcemaps setup [dir] [--dry-run] [--install] [--json]
154
+ autter-runtime sourcemaps verify <dir> [--endpoint <url>] [--url-prefix <prefix>]
155
+ [--ignore <glob>]... [--allow-maps] [--server] [--json]
156
+ autter-runtime sourcemaps inject <dir> [--dry-run] [--json]
71
157
  autter-runtime sourcemaps upload <dir> [--release <r>] [--url-prefix <prefix>]
72
- [--endpoint <url>] [--concurrency <n>] [--delete-after-upload] [--dry-run] [--json]
158
+ [--endpoint <url>] [--concurrency <n>] [--delete-after-upload]
159
+ [--no-sources-content] [--best-effort] [--dry-run] [--json]
73
160
  ```
74
161
 
75
162
  - **inject** writes `//# debugId=<id>`, a one-line snippet that registers
76
163
  `globalThis._autterDebugIds[new Error().stack] = "<id>"`, and `"debugId"` in
77
164
  the `.map`, for each `.js`/`.mjs`/`.cjs` with a map. The id is derived from
78
- sha256 of the bundle bytes; running it again changes nothing.
79
- - **upload** posts each map (without `sourcesContent`, gzipped, up to 32 MB)
80
- to `<endpoint>/v1/sourcemaps`, in parallel, skipping maps the ingester
81
- already has. Each map carries the bundle's function scopes (`x_autter_scopes`),
165
+ sha256 of the bundle bytes; running it again changes nothing. The browser
166
+ SDK and the Node and edge SDKs read the snippet's registry and send the ids
167
+ of the bundles in each exception's stack.
168
+ - **upload** posts each map (gzipped, up to 32 MB) to
169
+ `<endpoint>/v1/sourcemaps`, in parallel, skipping maps the ingester already
170
+ has. Each map carries the bundle's function scopes (`x_autter_scopes`),
82
171
  parsed from the file as served, so frames are named after the function they
83
172
  are in rather than their call site; a bundle that does not parse is uploaded
84
- without them, with a warning. Release: `--release`, else `GITHUB_SHA`,
173
+ without them, with a warning. It also carries the original source of your own
174
+ files (`sourcesContent`; never your dependencies', whole files only, as many
175
+ as fit the 32 MB limit), so issues show the lines around each frame;
176
+ `--no-sources-content` leaves it out. For server output, `--url-prefix` is
177
+ any tail of the path the server runs the files from (`/dist/` for
178
+ `/app/dist/server.js`), or `/`. Release: `--release`, else `GITHUB_SHA`,
85
179
  `VERCEL_GIT_COMMIT_SHA`, `CF_PAGES_COMMIT_SHA`, `GIT_SHA`, then
86
180
  `git rev-parse HEAD`.
181
+ - **upload --best-effort** is for build scripts that also run locally and in
182
+ fork PRs: without a key, or when the upload fails, it says so and exits 0;
183
+ with `--delete-after-upload`, every `.map` under `<dir>` is then deleted
184
+ anyway, so the output is the same with or without a key and never ships
185
+ maps.
87
186
 
88
187
  Exit codes: `0` ok, `1` some maps failed, `2` no key / no release / key
89
- rejected, `64` usage error.
188
+ rejected, `64` usage error (`--best-effort`: always `0`). `setup` and `verify`
189
+ have their own, above.
90
190
 
91
191
  ## Programmatic API
92
192
 
93
193
  ```ts
94
- import { injectDir, uploadDir } from "@autter/runtime-cli";
194
+ import { injectDir, uploadDir, verifyDir } from "@autter/runtime-cli";
95
195
 
96
196
  await injectDir("dist");
97
- const summary = await uploadDir("dist", { urlPrefix: "/", release: process.env.GIT_SHA });
197
+ const summary = await uploadDir("dist", { urlPrefix: "/", release: process.env.GIT_SHA, deleteAfterUpload: true });
198
+ const check = await verifyDir("dist"); // check.ok, check.failures, check.upload.status
98
199
  ```
@@ -69,12 +69,18 @@ declare function injectBundle(bundle: BundleWithMap, options?: InjectOptions): P
69
69
  declare function injectDir(dir: string, options?: InjectOptions): Promise<InjectResult[]>;
70
70
 
71
71
  declare const DEFAULT_ENDPOINT = "https://otlp.autter.dev";
72
- /** Largest map the ingester stores: characters of JSON without
73
- * `sourcesContent` (otlp-ingester's SOURCE_MAP_MAX_CHARS). */
72
+ /** Largest map the ingester stores (otlp-ingester's SOURCE_MAP_MAX_CHARS):
73
+ * characters of JSON without `sourcesContent`; the in-app sources' content
74
+ * that is sent fits in what is left of it, in UTF-8 bytes (storedMapJson). */
74
75
  declare const MAX_MAP_CHARS: number;
75
76
  /** The ingester's `/v1/sourcemaps` body limit, counted after the gzip body
76
77
  * is inflated: the map plus room for the rest of the request. */
77
78
  declare const MAX_UPLOAD_BYTES: number;
79
+ /** Keys `upload` accepts, and the one it refuses before sending anything. */
80
+ declare const UPLOAD_KEY_PREFIX = "autter_rtu_";
81
+ declare const SERVER_KEY_PREFIX = "autter_rt_";
82
+ declare const CLIENT_KEY_PREFIX = "autter_rtc_";
83
+ declare const KEY_ADVICE: string;
78
84
  interface UploadOptions {
79
85
  /** Only these bundles (absolute, or relative to the directory). */
80
86
  files?: string[];
@@ -86,7 +92,8 @@ interface UploadOptions {
86
92
  /** Ingester base URL. Default: $AUTTER_ENDPOINT, $AUTTER_RUNTIME_ENDPOINT,
87
93
  * $OTEL_EXPORTER_OTLP_ENDPOINT, else https://otlp.autter.dev. */
88
94
  endpoint?: string;
89
- /** Server ingest key. Default: $AUTTER_RUNTIME_KEY. */
95
+ /** Upload key (`autter_rtu_…`, recommended for CI) or server key
96
+ * (`autter_rt_…`). Default: $AUTTER_RUNTIME_KEY. */
90
97
  key?: string;
91
98
  /** List what would be uploaded; no network, no key needed. */
92
99
  dryRun?: boolean;
@@ -95,6 +102,11 @@ interface UploadOptions {
95
102
  /** Delete each `.map` (and its `sourceMappingURL` comment) once the
96
103
  * ingester has it, so maps are never deployed. */
97
104
  deleteAfterUpload?: boolean;
105
+ /** Send the original source of your own files (`sourcesContent`, when the
106
+ * bundler wrote it), so issues show the lines around each frame of your
107
+ * code. Dependencies' sources are never sent. Default true; false sends
108
+ * maps without it (`--no-sources-content`). */
109
+ sourcesContent?: boolean;
98
110
  log?: (line: string) => void;
99
111
  env?: NodeJS.ProcessEnv;
100
112
  cwd?: string;
@@ -132,8 +144,23 @@ interface UploadSummary {
132
144
  declare function resolveEndpoint(flag?: string, env?: NodeJS.ProcessEnv): string;
133
145
  /** prefix + path, with exactly one "/" between. "~/" (Sentry's spelling) means "/". */
134
146
  declare function servedFilename(urlPrefix: string | undefined, path: string): string;
135
- /** The map as the ingester stores it: `sourcesContent` removed, compact JSON. */
136
- declare function storedMapJson(map: Record<string, unknown>): string;
147
+ /** Whether a map's `sources` entry is the app's own code (the ingester's in-app path rule). */
148
+ declare function isInAppSource(path: string): boolean;
149
+ /**
150
+ * The `sourcesContent` the ingester keeps, or null to leave it out: in-app
151
+ * sources only (not ignore-listed, not under node_modules/ etc.), whole
152
+ * files, smallest first, while the JSON fits `limit` UTF-8 bytes given the
153
+ * `baseBytes` of the map without it.
154
+ */
155
+ declare function storedSourcesContent(fields: Record<string, unknown>, baseBytes: number, limit?: number): Array<string | null> | null;
156
+ /**
157
+ * The map as the ingester stores it, compact JSON: every field as given,
158
+ * then `sourcesContent` bounded by storedSourcesContent (left out when
159
+ * nothing is kept, or with `sourcesContent: false`).
160
+ */
161
+ declare function storedMapJson(map: Record<string, unknown>, options?: {
162
+ sourcesContent?: boolean;
163
+ }): string;
137
164
  declare class FatalUploadError extends Error {
138
165
  }
139
166
  /** Upload every source map under `dir` to the ingester's `/v1/sourcemaps`. */
@@ -149,7 +176,8 @@ interface AutterSourceMapsOptions {
149
176
  /** Ingester base URL. Default: $AUTTER_ENDPOINT, $AUTTER_RUNTIME_ENDPOINT,
150
177
  * $OTEL_EXPORTER_OTLP_ENDPOINT, else https://otlp.autter.dev. */
151
178
  endpoint?: string;
152
- /** Server ingest key. Default: $AUTTER_RUNTIME_KEY. Without one (and
179
+ /** Upload-only key (`autter_rtu_…`, recommended for CI) or server key
180
+ * (`autter_rt_…`). Default: $AUTTER_RUNTIME_KEY. Without one (and
153
181
  * without `upload: false` or `dryRun`), the plugin generates no source
154
182
  * maps it would have to delete again: the build is left as it was. */
155
183
  key?: string;
@@ -170,7 +198,26 @@ interface AutterSourceMapsOptions {
170
198
  failOnError?: boolean;
171
199
  /** No log output. */
172
200
  silent?: boolean;
201
+ /** Send your own files' original source with each map (`sourcesContent`,
202
+ * when the bundler wrote it), so issues show the lines around each frame
203
+ * of your code. Dependencies' sources are never sent. Default true. */
204
+ sourcesContent?: boolean;
205
+ /** Server bundles too (default true): Vite SSR builds and server
206
+ * environments, webpack builds for a Node target, and Next.js server
207
+ * output get debug ids and their maps uploaded, so errors in your backend
208
+ * resolve to its original files and lines. False leaves server output
209
+ * exactly as the bundler wrote it. */
210
+ serverBundles?: boolean;
173
211
  }
212
+ /**
213
+ * Where a server bundle directory's maps are stored: its path relative to
214
+ * the project (`/dist/server/`, `/.next/server/`), never the public URL
215
+ * prefix. The ingester matches a stored path as a suffix of the path the
216
+ * process runs the bundle from (`/app/dist/server/entry.js`,
217
+ * `/var/task/.next/server/app/page.js`), and the directory keeps server and
218
+ * browser maps of one release apart. "/" for a directory outside the project.
219
+ */
220
+ declare function serverUrlPrefix(projectRoot: string, dir: string): string;
174
221
  interface BuildOutputResult {
175
222
  injected: InjectResult[];
176
223
  upload: UploadSummary | null;
@@ -201,6 +248,9 @@ declare function processBuildOutput(input: {
201
248
  /** Map files the build emitted (absolute), when the bundler lists them;
202
249
  * the maps of `files` (or of every bundle under `dir`) are added. */
203
250
  maps?: string[];
251
+ /** Server output (RF-05): `urlPrefix` is serverUrlPrefix(), which the
252
+ * public `options.urlPrefix` never overrides. */
253
+ server?: boolean;
204
254
  }): Promise<BuildOutputResult>;
205
255
 
206
- export { type AutterSourceMapsOptions as A, BUNDLE_RE as B, DEFAULT_ENDPOINT as D, FatalUploadError as F, type InjectOptions as I, MAX_MAP_CHARS as M, SNIPPET_LINE_KEY as S, type UploadItem as U, type BuildOutputResult as a, type BundleWithMap as b, type InjectResult as c, MAX_UPLOAD_BYTES as d, type UploadOptions as e, type UploadSummary as f, findBundles as g, formatUploadSummary as h, injectBundle as i, injectDir as j, resolveEndpoint as k, shiftMappings as l, mapPathFor as m, snippetLine as n, storedMapJson as o, processBuildOutput as p, stripSourceMappingUrl as q, readSourceMappingUrl as r, servedFilename as s, uploadDir as u };
256
+ export { type AutterSourceMapsOptions as A, BUNDLE_RE as B, CLIENT_KEY_PREFIX as C, DEFAULT_ENDPOINT as D, FatalUploadError as F, type InjectOptions as I, KEY_ADVICE as K, MAX_MAP_CHARS as M, SERVER_KEY_PREFIX as S, UPLOAD_KEY_PREFIX as U, type BuildOutputResult as a, type BundleWithMap as b, type InjectResult as c, MAX_UPLOAD_BYTES as d, SNIPPET_LINE_KEY as e, type UploadItem as f, type UploadOptions as g, type UploadSummary as h, findBundles as i, formatUploadSummary as j, injectBundle as k, injectDir as l, isInAppSource as m, mapPathFor as n, resolveEndpoint as o, processBuildOutput as p, serverUrlPrefix as q, readSourceMappingUrl as r, servedFilename as s, shiftMappings as t, snippetLine as u, storedMapJson as v, storedSourcesContent as w, stripSourceMappingUrl as x, uploadDir as y };
@@ -69,12 +69,18 @@ declare function injectBundle(bundle: BundleWithMap, options?: InjectOptions): P
69
69
  declare function injectDir(dir: string, options?: InjectOptions): Promise<InjectResult[]>;
70
70
 
71
71
  declare const DEFAULT_ENDPOINT = "https://otlp.autter.dev";
72
- /** Largest map the ingester stores: characters of JSON without
73
- * `sourcesContent` (otlp-ingester's SOURCE_MAP_MAX_CHARS). */
72
+ /** Largest map the ingester stores (otlp-ingester's SOURCE_MAP_MAX_CHARS):
73
+ * characters of JSON without `sourcesContent`; the in-app sources' content
74
+ * that is sent fits in what is left of it, in UTF-8 bytes (storedMapJson). */
74
75
  declare const MAX_MAP_CHARS: number;
75
76
  /** The ingester's `/v1/sourcemaps` body limit, counted after the gzip body
76
77
  * is inflated: the map plus room for the rest of the request. */
77
78
  declare const MAX_UPLOAD_BYTES: number;
79
+ /** Keys `upload` accepts, and the one it refuses before sending anything. */
80
+ declare const UPLOAD_KEY_PREFIX = "autter_rtu_";
81
+ declare const SERVER_KEY_PREFIX = "autter_rt_";
82
+ declare const CLIENT_KEY_PREFIX = "autter_rtc_";
83
+ declare const KEY_ADVICE: string;
78
84
  interface UploadOptions {
79
85
  /** Only these bundles (absolute, or relative to the directory). */
80
86
  files?: string[];
@@ -86,7 +92,8 @@ interface UploadOptions {
86
92
  /** Ingester base URL. Default: $AUTTER_ENDPOINT, $AUTTER_RUNTIME_ENDPOINT,
87
93
  * $OTEL_EXPORTER_OTLP_ENDPOINT, else https://otlp.autter.dev. */
88
94
  endpoint?: string;
89
- /** Server ingest key. Default: $AUTTER_RUNTIME_KEY. */
95
+ /** Upload key (`autter_rtu_…`, recommended for CI) or server key
96
+ * (`autter_rt_…`). Default: $AUTTER_RUNTIME_KEY. */
90
97
  key?: string;
91
98
  /** List what would be uploaded; no network, no key needed. */
92
99
  dryRun?: boolean;
@@ -95,6 +102,11 @@ interface UploadOptions {
95
102
  /** Delete each `.map` (and its `sourceMappingURL` comment) once the
96
103
  * ingester has it, so maps are never deployed. */
97
104
  deleteAfterUpload?: boolean;
105
+ /** Send the original source of your own files (`sourcesContent`, when the
106
+ * bundler wrote it), so issues show the lines around each frame of your
107
+ * code. Dependencies' sources are never sent. Default true; false sends
108
+ * maps without it (`--no-sources-content`). */
109
+ sourcesContent?: boolean;
98
110
  log?: (line: string) => void;
99
111
  env?: NodeJS.ProcessEnv;
100
112
  cwd?: string;
@@ -132,8 +144,23 @@ interface UploadSummary {
132
144
  declare function resolveEndpoint(flag?: string, env?: NodeJS.ProcessEnv): string;
133
145
  /** prefix + path, with exactly one "/" between. "~/" (Sentry's spelling) means "/". */
134
146
  declare function servedFilename(urlPrefix: string | undefined, path: string): string;
135
- /** The map as the ingester stores it: `sourcesContent` removed, compact JSON. */
136
- declare function storedMapJson(map: Record<string, unknown>): string;
147
+ /** Whether a map's `sources` entry is the app's own code (the ingester's in-app path rule). */
148
+ declare function isInAppSource(path: string): boolean;
149
+ /**
150
+ * The `sourcesContent` the ingester keeps, or null to leave it out: in-app
151
+ * sources only (not ignore-listed, not under node_modules/ etc.), whole
152
+ * files, smallest first, while the JSON fits `limit` UTF-8 bytes given the
153
+ * `baseBytes` of the map without it.
154
+ */
155
+ declare function storedSourcesContent(fields: Record<string, unknown>, baseBytes: number, limit?: number): Array<string | null> | null;
156
+ /**
157
+ * The map as the ingester stores it, compact JSON: every field as given,
158
+ * then `sourcesContent` bounded by storedSourcesContent (left out when
159
+ * nothing is kept, or with `sourcesContent: false`).
160
+ */
161
+ declare function storedMapJson(map: Record<string, unknown>, options?: {
162
+ sourcesContent?: boolean;
163
+ }): string;
137
164
  declare class FatalUploadError extends Error {
138
165
  }
139
166
  /** Upload every source map under `dir` to the ingester's `/v1/sourcemaps`. */
@@ -149,7 +176,8 @@ interface AutterSourceMapsOptions {
149
176
  /** Ingester base URL. Default: $AUTTER_ENDPOINT, $AUTTER_RUNTIME_ENDPOINT,
150
177
  * $OTEL_EXPORTER_OTLP_ENDPOINT, else https://otlp.autter.dev. */
151
178
  endpoint?: string;
152
- /** Server ingest key. Default: $AUTTER_RUNTIME_KEY. Without one (and
179
+ /** Upload-only key (`autter_rtu_…`, recommended for CI) or server key
180
+ * (`autter_rt_…`). Default: $AUTTER_RUNTIME_KEY. Without one (and
153
181
  * without `upload: false` or `dryRun`), the plugin generates no source
154
182
  * maps it would have to delete again: the build is left as it was. */
155
183
  key?: string;
@@ -170,7 +198,26 @@ interface AutterSourceMapsOptions {
170
198
  failOnError?: boolean;
171
199
  /** No log output. */
172
200
  silent?: boolean;
201
+ /** Send your own files' original source with each map (`sourcesContent`,
202
+ * when the bundler wrote it), so issues show the lines around each frame
203
+ * of your code. Dependencies' sources are never sent. Default true. */
204
+ sourcesContent?: boolean;
205
+ /** Server bundles too (default true): Vite SSR builds and server
206
+ * environments, webpack builds for a Node target, and Next.js server
207
+ * output get debug ids and their maps uploaded, so errors in your backend
208
+ * resolve to its original files and lines. False leaves server output
209
+ * exactly as the bundler wrote it. */
210
+ serverBundles?: boolean;
173
211
  }
212
+ /**
213
+ * Where a server bundle directory's maps are stored: its path relative to
214
+ * the project (`/dist/server/`, `/.next/server/`), never the public URL
215
+ * prefix. The ingester matches a stored path as a suffix of the path the
216
+ * process runs the bundle from (`/app/dist/server/entry.js`,
217
+ * `/var/task/.next/server/app/page.js`), and the directory keeps server and
218
+ * browser maps of one release apart. "/" for a directory outside the project.
219
+ */
220
+ declare function serverUrlPrefix(projectRoot: string, dir: string): string;
174
221
  interface BuildOutputResult {
175
222
  injected: InjectResult[];
176
223
  upload: UploadSummary | null;
@@ -201,6 +248,9 @@ declare function processBuildOutput(input: {
201
248
  /** Map files the build emitted (absolute), when the bundler lists them;
202
249
  * the maps of `files` (or of every bundle under `dir`) are added. */
203
250
  maps?: string[];
251
+ /** Server output (RF-05): `urlPrefix` is serverUrlPrefix(), which the
252
+ * public `options.urlPrefix` never overrides. */
253
+ server?: boolean;
204
254
  }): Promise<BuildOutputResult>;
205
255
 
206
- export { type AutterSourceMapsOptions as A, BUNDLE_RE as B, DEFAULT_ENDPOINT as D, FatalUploadError as F, type InjectOptions as I, MAX_MAP_CHARS as M, SNIPPET_LINE_KEY as S, type UploadItem as U, type BuildOutputResult as a, type BundleWithMap as b, type InjectResult as c, MAX_UPLOAD_BYTES as d, type UploadOptions as e, type UploadSummary as f, findBundles as g, formatUploadSummary as h, injectBundle as i, injectDir as j, resolveEndpoint as k, shiftMappings as l, mapPathFor as m, snippetLine as n, storedMapJson as o, processBuildOutput as p, stripSourceMappingUrl as q, readSourceMappingUrl as r, servedFilename as s, uploadDir as u };
256
+ export { type AutterSourceMapsOptions as A, BUNDLE_RE as B, CLIENT_KEY_PREFIX as C, DEFAULT_ENDPOINT as D, FatalUploadError as F, type InjectOptions as I, KEY_ADVICE as K, MAX_MAP_CHARS as M, SERVER_KEY_PREFIX as S, UPLOAD_KEY_PREFIX as U, type BuildOutputResult as a, type BundleWithMap as b, type InjectResult as c, MAX_UPLOAD_BYTES as d, SNIPPET_LINE_KEY as e, type UploadItem as f, type UploadOptions as g, type UploadSummary as h, findBundles as i, formatUploadSummary as j, injectBundle as k, injectDir as l, isInAppSource as m, mapPathFor as n, resolveEndpoint as o, processBuildOutput as p, serverUrlPrefix as q, readSourceMappingUrl as r, servedFilename as s, shiftMappings as t, snippetLine as u, storedMapJson as v, storedSourcesContent as w, stripSourceMappingUrl as x, uploadDir as y };
@@ -667,6 +667,10 @@ var DEFAULT_ENDPOINT = "https://otlp.autter.dev";
667
667
  var MAX_MAP_CHARS = 32 * 1024 * 1024;
668
668
  var MAX_UPLOAD_BYTES = MAX_MAP_CHARS + 1024 * 1024;
669
669
  var LEGACY_MAX_BYTES = 5 * 1024 * 1024;
670
+ var UPLOAD_KEY_PREFIX = "autter_rtu_";
671
+ var SERVER_KEY_PREFIX = "autter_rt_";
672
+ var CLIENT_KEY_PREFIX = "autter_rtc_";
673
+ var KEY_ADVICE = "set AUTTER_RUNTIME_KEY to an upload-only key (autter_rtu_\u2026, recommended for CI: it can only upload source maps) or the full server key (autter_rt_\u2026, also works)";
670
674
  var gzip = promisify(gzipCallback);
671
675
  var CHECK_BATCH = 500;
672
676
  var MAX_ATTEMPTS = 3;
@@ -680,9 +684,56 @@ function servedFilename(urlPrefix, path) {
680
684
  if (!/^[a-z][\w+.-]*:\/\//i.test(prefix) && !prefix.startsWith("/")) prefix = `/${prefix}`;
681
685
  return `${prefix.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
682
686
  }
683
- function storedMapJson(map) {
684
- const { sourcesContent: _omitted, ...safe } = map;
685
- return JSON.stringify(safe);
687
+ var NOT_IN_APP_PATH_PARTS = ["node_modules/", "/site-packages/", "/dist-packages/", "/vendor/", "/.pnpm/"];
688
+ var NOT_IN_APP_PATH_PREFIXES = ["node:", "internal/"];
689
+ var EXTENSION_SCHEMES = ["chrome-extension:", "moz-extension:", "safari-extension:", "safari-web-extension:", "webkit-masked-url:"];
690
+ function isInAppSource(path) {
691
+ if (!path) return true;
692
+ const normalized = path.replace(/\\/g, "/");
693
+ if (NOT_IN_APP_PATH_PARTS.some((part) => normalized.includes(part))) return false;
694
+ if (NOT_IN_APP_PATH_PREFIXES.some((prefix) => normalized.startsWith(prefix))) return false;
695
+ const lower = normalized.toLowerCase();
696
+ return !EXTENSION_SCHEMES.some((scheme) => lower.startsWith(scheme));
697
+ }
698
+ var SOURCES_CONTENT_WRAPPER_BYTES = Buffer.byteLength(',"sourcesContent":[]');
699
+ var NULL_ENTRY_BYTES = 4;
700
+ function storedSourcesContent(fields, baseBytes, limit = MAX_MAP_CHARS) {
701
+ const { sources, sourcesContent } = fields;
702
+ if (!Array.isArray(sources) || !Array.isArray(sourcesContent) || sources.length === 0) return null;
703
+ const ignored = /* @__PURE__ */ new Set();
704
+ for (const list of [fields.ignoreList, fields.x_google_ignoreList]) {
705
+ if (!Array.isArray(list)) continue;
706
+ for (const index of list) if (Number.isInteger(index)) ignored.add(index);
707
+ }
708
+ const count = sources.length;
709
+ const candidates = [];
710
+ for (let index = 0; index < count; index++) {
711
+ const source = sources[index];
712
+ const content = sourcesContent[index];
713
+ if (typeof source !== "string" || typeof content !== "string") continue;
714
+ if (ignored.has(index) || !isInAppSource(source)) continue;
715
+ candidates.push({ index, content, bytes: Buffer.byteLength(JSON.stringify(content)) });
716
+ }
717
+ if (candidates.length === 0) return null;
718
+ candidates.sort((a, b) => a.bytes - b.bytes || a.index - b.index);
719
+ let remaining = limit - baseBytes - SOURCES_CONTENT_WRAPPER_BYTES - NULL_ENTRY_BYTES * count - (count - 1);
720
+ const kept = new Array(count).fill(null);
721
+ let any = false;
722
+ for (const candidate of candidates) {
723
+ const extra = candidate.bytes - NULL_ENTRY_BYTES;
724
+ if (extra > remaining) break;
725
+ remaining -= extra;
726
+ kept[candidate.index] = candidate.content;
727
+ any = true;
728
+ }
729
+ return any ? kept : null;
730
+ }
731
+ function storedMapJson(map, options = {}) {
732
+ const { sourcesContent: _content, ...rest } = map;
733
+ const base = JSON.stringify(rest);
734
+ if (options.sourcesContent === false) return base;
735
+ const content = storedSourcesContent(map, Buffer.byteLength(base));
736
+ return content ? JSON.stringify({ ...rest, sourcesContent: content }) : base;
686
737
  }
687
738
  function bytesOf(buffer) {
688
739
  return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
@@ -693,7 +744,7 @@ function fits(json, body) {
693
744
  function uploadBody(release, filename, debugId, json) {
694
745
  return `{"release":${JSON.stringify(release)},"filename":${JSON.stringify(filename)},${debugId ? `"debugId":${JSON.stringify(debugId)},` : ""}"map":${json}}`;
695
746
  }
696
- async function prepare(bundle, urlPrefix, release, log, compress) {
747
+ async function prepare(bundle, urlPrefix, release, log, compress, sourcesContent) {
697
748
  let map;
698
749
  try {
699
750
  map = JSON.parse(await readFile3(bundle.map, "utf8"));
@@ -708,9 +759,10 @@ async function prepare(bundle, urlPrefix, release, log, compress) {
708
759
  const filename = servedFilename(urlPrefix, bundle.path);
709
760
  const { [SCOPES_KEY]: _stale, ...plain } = map;
710
761
  const scopes = computeScopes(code, map);
711
- let json = storedMapJson(plain);
762
+ const stored = { sourcesContent };
763
+ let json = storedMapJson(plain, stored);
712
764
  if ("scopes" in scopes) {
713
- const scoped = storedMapJson({ ...plain, [SCOPES_KEY]: scopes.scopes });
765
+ const scoped = storedMapJson({ ...plain, [SCOPES_KEY]: scopes.scopes }, stored);
714
766
  if (fits(scoped, uploadBody(release, filename, debugId, scoped))) json = scoped;
715
767
  else if (fits(json, uploadBody(release, filename, debugId, json))) {
716
768
  log(`warning: ${bundle.path}: its map is over 32 MB with function scopes, so it is uploaded without them (frames keep call-site names)`);
@@ -757,7 +809,7 @@ async function uploadDir(dir, options = {}) {
757
809
  };
758
810
  const items = [];
759
811
  for (const bundle of await findBundles(dir, options.files)) {
760
- const prepared = await prepare(bundle, options.urlPrefix, release, log, !options.dryRun);
812
+ const prepared = await prepare(bundle, options.urlPrefix, release, log, !options.dryRun, options.sourcesContent !== false);
761
813
  if ("error" in prepared) {
762
814
  summary.failed.push({ path: bundle.path, filename: servedFilename(options.urlPrefix, bundle.path), error: prepared.error });
763
815
  } else if (prepared.mapChars > MAX_MAP_CHARS || prepared.bytes > MAX_UPLOAD_BYTES) {
@@ -775,10 +827,13 @@ async function uploadDir(dir, options = {}) {
775
827
  return summary;
776
828
  }
777
829
  const key = (options.key ?? env.AUTTER_RUNTIME_KEY ?? "").trim();
778
- if (!key) throw new FatalUploadError("no ingest key: set AUTTER_RUNTIME_KEY to a server key (autter_rt_\u2026)");
779
- if (key.startsWith("autter_rtc_")) {
780
- throw new FatalUploadError("AUTTER_RUNTIME_KEY is a publishable client key (autter_rtc_\u2026); uploads need a server key (autter_rt_\u2026)");
830
+ if (!key) throw new FatalUploadError(`no ingest key: ${KEY_ADVICE}`);
831
+ if (key.startsWith(CLIENT_KEY_PREFIX)) {
832
+ throw new FatalUploadError(`AUTTER_RUNTIME_KEY is a publishable client key (autter_rtc_\u2026), which cannot upload source maps: ${KEY_ADVICE}`);
781
833
  }
834
+ const rejected = (status, detail) => new FatalUploadError(
835
+ `the ingester rejected the key (${status}): ${detail}` + (key.startsWith(UPLOAD_KEY_PREFIX) ? " (upload keys, autter_rtu_\u2026, need ingester 1.7.0 or later; the server key works with any)" : "")
836
+ );
782
837
  const headers = { "content-type": "application/json", authorization: `Bearer ${key}` };
783
838
  const uploadHeaders = { ...headers, "content-encoding": "gzip" };
784
839
  const skipped = /* @__PURE__ */ new Set();
@@ -800,9 +855,7 @@ async function uploadDir(dir, options = {}) {
800
855
  );
801
856
  break;
802
857
  }
803
- if (res.status === 401 || res.status === 403) {
804
- throw new FatalUploadError(`the ingester rejected the key (${res.status}): ${(await res.text()).slice(0, 300)}`);
805
- }
858
+ if (res.status === 401 || res.status === 403) throw rejected(res.status, (await res.text()).slice(0, 300));
806
859
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
807
860
  const body = await res.json();
808
861
  const present = new Set(Array.isArray(body.present) ? body.present : []);
@@ -833,7 +886,7 @@ async function uploadDir(dir, options = {}) {
833
886
  detail = err.message;
834
887
  }
835
888
  if (status === 401 || status === 403) {
836
- fatal = new FatalUploadError(`the ingester rejected the key (${status}): ${detail}`);
889
+ fatal = rejected(status, detail);
837
890
  return;
838
891
  }
839
892
  const retryable = status === 0 || status === 429 || status >= 500;
@@ -876,7 +929,11 @@ function strip(item) {
876
929
 
877
930
  // src/build.ts
878
931
  import { existsSync as existsSync2 } from "fs";
879
- import { relative as relative2, sep as sep2 } from "path";
932
+ import { isAbsolute, relative as relative2, sep as sep2 } from "path";
933
+ function serverUrlPrefix(projectRoot, dir) {
934
+ const rel = relative2(projectRoot, dir).split(sep2).join("/");
935
+ return !rel || rel.startsWith("..") || isAbsolute(rel) ? "/" : `/${rel.replace(/\/+$/, "")}/`;
936
+ }
880
937
  function buildKey(options) {
881
938
  const key = (options.key ?? process.env.AUTTER_RUNTIME_KEY ?? "").trim();
882
939
  return key || null;
@@ -939,12 +996,13 @@ async function processBuildOutput(input) {
939
996
  result.upload = await uploadDir(input.dir, {
940
997
  files: input.files,
941
998
  release: options.release,
942
- urlPrefix: options.urlPrefix ?? input.urlPrefix,
999
+ urlPrefix: input.server ? input.urlPrefix : options.urlPrefix ?? input.urlPrefix,
943
1000
  endpoint: options.endpoint,
944
1001
  key: key ?? void 0,
945
1002
  dryRun: options.dryRun,
946
1003
  concurrency: options.concurrency,
947
1004
  deleteAfterUpload: options.deleteAfterUpload ?? input.deleteAfterUpload,
1005
+ ...options.sourcesContent === false ? { sourcesContent: false } : {},
948
1006
  log
949
1007
  });
950
1008
  for (const line of formatUploadSummary(result.upload)) log(line);
@@ -1023,11 +1081,18 @@ export {
1023
1081
  DEFAULT_ENDPOINT,
1024
1082
  MAX_MAP_CHARS,
1025
1083
  MAX_UPLOAD_BYTES,
1084
+ UPLOAD_KEY_PREFIX,
1085
+ SERVER_KEY_PREFIX,
1086
+ CLIENT_KEY_PREFIX,
1087
+ KEY_ADVICE,
1026
1088
  resolveEndpoint,
1027
1089
  servedFilename,
1090
+ isInAppSource,
1091
+ storedSourcesContent,
1028
1092
  storedMapJson,
1029
1093
  FatalUploadError,
1030
1094
  uploadDir,
1095
+ serverUrlPrefix,
1031
1096
  shouldGenerateMaps,
1032
1097
  logMapsNotGenerated,
1033
1098
  formatUploadSummary,