-
-
Notifications
You must be signed in to change notification settings - Fork 4
feat: add sentry sourcemap inject and sentry sourcemap upload commands
#547
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 6 commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
34bc2c7
feat: add `sentry sourcemap inject` and `sentry sourcemap upload` com…
BYK 91cd4af
fix: address review feedback for sourcemap CLI commands
BYK 06e28a2
fix: preserve directory structure in upload URLs, hide sourcemaps alias
BYK 0923df2
chore: regenerate skill files
github-actions[bot] 4e40e4a
fix: normalize paths for cross-platform artifact URLs
BYK 9845723
fix: dry-run correctly reports existing debug IDs, remove dead filter
BYK 27e5fb1
fix: deduplicate EXISTING_DEBUGID_RE, clarify upload auto-inject docs
BYK eda15a6
chore: retrigger CI
BYK File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
30 changes: 30 additions & 0 deletions
30
plugins/sentry-cli/skills/sentry-cli/references/sourcemap.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| --- | ||
| name: sentry-cli-sourcemap | ||
| version: 0.21.0-dev.0 | ||
| description: Sentry CLI sourcemap commands | ||
| requires: | ||
| bins: ["sentry"] | ||
| auth: true | ||
| --- | ||
|
|
||
| # sourcemap Commands | ||
|
|
||
| Manage sourcemaps | ||
|
|
||
| ### `sentry sourcemap inject <directory>` | ||
|
|
||
| Inject debug IDs into JavaScript files and sourcemaps | ||
|
|
||
| **Flags:** | ||
| - `--ext <value> - Comma-separated file extensions to process (default: .js,.cjs,.mjs)` | ||
| - `--dry-run - Show what would be modified without writing` | ||
|
|
||
| ### `sentry sourcemap upload <directory>` | ||
|
|
||
| Upload sourcemaps to Sentry | ||
|
|
||
| **Flags:** | ||
| - `--release <value> - Release version to associate with the upload` | ||
| - `--url-prefix <value> - URL prefix for uploaded files (default: ~/) - (default: "~/")` | ||
|
|
||
| All commands also support `--json`, `--fields`, `--help`, `--log-level`, and `--verbose` flags. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,136 +1,12 @@ | ||
| /** | ||
| * Debug ID injection for JavaScript sourcemaps. | ||
| * Debug ID injection — re-exports from src/lib/sourcemap/debug-id.ts. | ||
| * | ||
| * Injects Sentry debug IDs into JavaScript files and their companion | ||
| * sourcemaps for reliable server-side stack trace resolution. Debug IDs | ||
| * replace fragile filename/release-based sourcemap matching with a | ||
| * deterministic UUID embedded in both the JS file and its sourcemap. | ||
| * | ||
| * The UUID algorithm and runtime snippet are byte-for-byte compatible | ||
| * with `@sentry/bundler-plugin-core`'s `stringToUUID` and | ||
| * `getDebugIdSnippet` — see ECMA-426 (Source Map Format) for the spec. | ||
| */ | ||
|
|
||
| import { createHash } from "node:crypto"; | ||
| import { readFile, writeFile } from "node:fs/promises"; | ||
|
|
||
| /** Comment prefix used to identify an existing debug ID in a JS file. */ | ||
| const DEBUGID_COMMENT_PREFIX = "//# debugId="; | ||
|
|
||
| /** Regex to extract an existing debug ID from a JS file. */ | ||
| const EXISTING_DEBUGID_RE = /\/\/# debugId=([0-9a-fA-F-]{36})/; | ||
|
|
||
| /** | ||
| * Generate a deterministic debug ID (UUID v4 format) from content. | ||
| * | ||
| * Computes SHA-256 of the input, then formats the first 128 bits as a | ||
| * UUID v4 string. Matches `@sentry/bundler-plugin-core`'s `stringToUUID` | ||
| * exactly — position 12 is forced to `4` (version), and position 16 is | ||
| * forced to one of `8/9/a/b` (variant, RFC 4122 §4.4). | ||
| * | ||
| * @param content - File content (string or Buffer) to hash | ||
| * @returns UUID v4 string, e.g. `"a1b2c3d4-e5f6-4789-abcd-ef0123456789"` | ||
| */ | ||
| export function contentToDebugId(content: string | Buffer): string { | ||
| const hash = createHash("sha256").update(content).digest("hex"); | ||
| // Position 16 (first char of 5th group in the hash) determines the | ||
| // variant nibble. charCodeAt(0) of a hex digit is deterministic. | ||
| const v4variant = ["8", "9", "a", "b"][ | ||
| hash.substring(16, 17).charCodeAt(0) % 4 | ||
| ]; | ||
| return `${hash.substring(0, 8)}-${hash.substring(8, 12)}-4${hash.substring(13, 16)}-${v4variant}${hash.substring(17, 20)}-${hash.substring(20, 32)}`.toLowerCase(); | ||
| } | ||
|
|
||
| /** | ||
| * Build the runtime IIFE snippet that registers a debug ID in | ||
| * `globalThis._sentryDebugIds`. | ||
| * | ||
| * At runtime, the Sentry SDK reads this map (keyed by Error stack traces) | ||
| * to associate stack frames with their debug IDs, which are then used to | ||
| * look up the correct sourcemap on the server. | ||
| * | ||
| * The snippet is a single-line IIFE so it only adds one line to the | ||
| * sourcemap mappings offset. | ||
| * | ||
| * @param debugId - The UUID to embed | ||
| * @returns Minified IIFE string (single line, starts with `;`) | ||
| */ | ||
| export function getDebugIdSnippet(debugId: string): string { | ||
| return `;!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="${debugId}",e._sentryDebugIdIdentifier="sentry-dbid-${debugId}")}catch(e){}}();`; | ||
| } | ||
|
|
||
| /** | ||
| * Inject a Sentry debug ID into a JavaScript file and its companion | ||
| * sourcemap. | ||
| * | ||
| * Performs four mutations: | ||
| * 1. Prepends the runtime snippet to the JS file (after any hashbang) | ||
| * 2. Appends a `//# debugId=<uuid>` comment to the JS file | ||
| * 3. Prepends a `;` to the sourcemap `mappings` (offsets by one line) | ||
| * 4. Adds `debug_id` and `debugId` fields to the sourcemap JSON | ||
| * | ||
| * The operation is **idempotent** — files that already contain a | ||
| * `//# debugId=` comment are returned unchanged. | ||
| * | ||
| * @param jsPath - Path to the JavaScript file | ||
| * @param mapPath - Path to the companion `.map` file | ||
| * @returns The debug ID that was injected (or already present) | ||
| * Build scripts import from here for convenience. The actual | ||
| * implementation lives in src/lib/sourcemap/ alongside the ZIP builder | ||
| * and injection utilities. | ||
| */ | ||
| export async function injectDebugId( | ||
| jsPath: string, | ||
| mapPath: string | ||
| ): Promise<{ debugId: string }> { | ||
| const [jsContent, mapContent] = await Promise.all([ | ||
| readFile(jsPath, "utf-8"), | ||
| readFile(mapPath, "utf-8"), | ||
| ]); | ||
|
|
||
| // Idempotent: if the JS file already has a debug ID, extract and return it | ||
| const existingMatch = jsContent.match(EXISTING_DEBUGID_RE); | ||
| if (existingMatch) { | ||
| return { debugId: existingMatch[1] }; | ||
| } | ||
|
|
||
| // Generate debug ID from the sourcemap content (deterministic) | ||
| const debugId = contentToDebugId(mapContent); | ||
| const snippet = getDebugIdSnippet(debugId); | ||
|
|
||
| // --- Mutate JS file --- | ||
| // Preserve hashbang if present, insert snippet after it | ||
| let newJs: string; | ||
| if (jsContent.startsWith("#!")) { | ||
| const newlineIdx = jsContent.indexOf("\n"); | ||
| // Handle hashbang without trailing newline (entire file is the #! line) | ||
| const splitAt = newlineIdx === -1 ? jsContent.length : newlineIdx + 1; | ||
| const hashbang = jsContent.slice(0, splitAt); | ||
| const rest = jsContent.slice(splitAt); | ||
| const sep = newlineIdx === -1 ? "\n" : ""; | ||
| newJs = `${hashbang}${sep}${snippet}\n${rest}`; | ||
| } else { | ||
| newJs = `${snippet}\n${jsContent}`; | ||
| } | ||
| // Append debug ID comment at the end | ||
| newJs += `\n${DEBUGID_COMMENT_PREFIX}${debugId}\n`; | ||
|
|
||
| // --- Mutate sourcemap --- | ||
| // Parse, adjust mappings, add debug ID fields | ||
| const map = JSON.parse(mapContent) as { | ||
| mappings: string; | ||
| debug_id?: string; | ||
| debugId?: string; | ||
| }; | ||
| // Prepend one `;` to mappings — tells decoders "no mappings for the | ||
| // first line" (the injected snippet line). Each `;` in VLQ mappings | ||
| // represents a line boundary. | ||
| map.mappings = `;${map.mappings}`; | ||
| map.debug_id = debugId; | ||
| map.debugId = debugId; | ||
|
|
||
| // Write both files concurrently | ||
| await Promise.all([ | ||
| writeFile(jsPath, newJs), | ||
| writeFile(mapPath, JSON.stringify(map)), | ||
| ]); | ||
|
|
||
| return { debugId }; | ||
| } | ||
| export { | ||
| contentToDebugId, | ||
| getDebugIdSnippet, | ||
| injectDebugId, | ||
| } from "../src/lib/sourcemap/debug-id.js"; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| import { buildRouteMap } from "@stricli/core"; | ||
| import { injectCommand } from "./inject.js"; | ||
| import { uploadCommand } from "./upload.js"; | ||
|
|
||
| export const sourcemapRoute = buildRouteMap({ | ||
| routes: { | ||
| inject: injectCommand, | ||
| upload: uploadCommand, | ||
| }, | ||
| docs: { | ||
| brief: "Manage sourcemaps", | ||
| fullDescription: | ||
| "Inject debug IDs and upload sourcemaps to Sentry.\n\n" + | ||
| "Alias: `sentry sourcemaps` → `sentry sourcemap`", | ||
| hideRoute: {}, | ||
| }, | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,120 @@ | ||
| /** | ||
| * sentry sourcemap inject <dir> | ||
| * | ||
| * Scan a directory for JavaScript files and their companion sourcemaps, | ||
| * then inject Sentry debug IDs for reliable sourcemap resolution. | ||
| */ | ||
|
|
||
| import type { SentryContext } from "../../context.js"; | ||
| import { buildCommand } from "../../lib/command.js"; | ||
| import { | ||
| colorTag, | ||
| mdKvTable, | ||
| renderMarkdown, | ||
| } from "../../lib/formatters/markdown.js"; | ||
| import { CommandOutput } from "../../lib/formatters/output.js"; | ||
| import { | ||
| type InjectResult, | ||
| injectDirectory, | ||
| } from "../../lib/sourcemap/inject.js"; | ||
|
|
||
| /** Result type for the inject command output. */ | ||
| type InjectCommandResult = { | ||
| modified: number; | ||
| skipped: number; | ||
| files: InjectResult[]; | ||
| }; | ||
|
|
||
| /** Format human-readable output for inject results. */ | ||
| function formatInjectResult(data: InjectCommandResult): string { | ||
| const lines: string[] = []; | ||
| lines.push( | ||
| mdKvTable([ | ||
| ["Files modified", String(data.modified)], | ||
| ["Files skipped", String(data.skipped)], | ||
| ]) | ||
| ); | ||
|
|
||
| if (data.files.length > 0) { | ||
| lines.push(""); | ||
| for (const file of data.files) { | ||
| const status = file.injected ? "✓" : "–"; | ||
| lines.push( | ||
| `${status} ${file.jsPath} → ${colorTag("muted", file.debugId)}` | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| return renderMarkdown(lines.join("\n")); | ||
| } | ||
|
|
||
| export const injectCommand = buildCommand({ | ||
| docs: { | ||
| brief: "Inject debug IDs into JavaScript files and sourcemaps", | ||
| fullDescription: | ||
| "Scans a directory for .js/.mjs/.cjs files and their companion .map files, " + | ||
| "then injects Sentry debug IDs for reliable sourcemap resolution.\n\n" + | ||
| "The injection is idempotent — files that already have debug IDs are skipped.\n\n" + | ||
| "Usage:\n" + | ||
| " sentry sourcemap inject ./dist\n" + | ||
| " sentry sourcemap inject ./build --ext .js,.mjs\n" + | ||
| " sentry sourcemap inject ./out --dry-run", | ||
| }, | ||
| output: { | ||
| human: formatInjectResult, | ||
| }, | ||
| parameters: { | ||
| positional: { | ||
| kind: "tuple", | ||
| parameters: [ | ||
| { | ||
| brief: "Directory to scan for JS + sourcemap pairs", | ||
| parse: String, | ||
| placeholder: "directory", | ||
| }, | ||
| ], | ||
| }, | ||
| flags: { | ||
| ext: { | ||
| kind: "parsed", | ||
| parse: String, | ||
| brief: | ||
| "Comma-separated file extensions to process (default: .js,.cjs,.mjs)", | ||
| optional: true, | ||
| }, | ||
| "dry-run": { | ||
| kind: "boolean", | ||
| brief: "Show what would be modified without writing", | ||
| optional: true, | ||
| default: false, | ||
| }, | ||
| }, | ||
| }, | ||
| async *func( | ||
| this: SentryContext, | ||
| flags: { ext?: string; "dry-run"?: boolean }, | ||
| dir: string | ||
| ) { | ||
| const extensions = flags.ext?.split(",").map((e) => e.trim()); | ||
| const results = await injectDirectory(dir, { | ||
| extensions, | ||
| dryRun: flags["dry-run"], | ||
| }); | ||
|
|
||
| const modified = results.filter((r) => r.injected).length; | ||
| const skipped = results.length - modified; | ||
|
|
||
| yield new CommandOutput<InjectCommandResult>({ | ||
| modified, | ||
| skipped, | ||
| files: results, | ||
| }); | ||
|
|
||
| if (modified > 0) { | ||
| return { | ||
| hint: "Run `sentry sourcemap upload` to upload the injected files to Sentry", | ||
| }; | ||
| } | ||
| return {}; | ||
| }, | ||
| }); |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.