-
Notifications
You must be signed in to change notification settings - Fork 57
feat(core): Add experimental debug ID based source map upload to Rollup and Vite plugins #192
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
Changes from 8 commits
f9d76ce
0de0ea7
f7ed8ad
31e4136
f907096
830b0bc
4907328
88ae11b
ebf0a92
4f31a3f
98300c2
025e4c1
22f56d9
4154b4a
a7cf4e3
befc007
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,149 @@ | ||
| import * as fs from "fs"; | ||
| import MagicString from "magic-string"; | ||
| import * as path from "path"; | ||
| import * as util from "util"; | ||
| import { Logger } from "./sentry/logger"; | ||
| import { stringToUUID } from "./utils"; | ||
|
|
||
| // TODO: Find a more elaborate process to generate this. (Maybe with type checking and built-in minification) | ||
| const DEBUG_ID_INJECTOR_SNIPPET = | ||
| ';!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{},n=(new Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="__SENTRY_DEBUG_ID__",e._sentryDebugIdIdentifier="sdbid-__SENTRY_DEBUG_ID__")}catch(e){}}();'; | ||
|
|
||
| export function injectDebugIdSnippetIntoChunk(code: string) { | ||
| const debugId = stringToUUID(code); // generate a deterministic debug ID | ||
| const ms = new MagicString(code); | ||
|
|
||
| const codeToInject = DEBUG_ID_INJECTOR_SNIPPET.replace(/__SENTRY_DEBUG_ID__/g, debugId); | ||
|
|
||
| // We need to be careful not to inject the snippet before any `"use strict";`s. | ||
| // As an additional complication `"use strict";`s may come after any number of comments. | ||
| const commentUseStrictRegex = | ||
| /^(?:\s*|\/\*(.|\r|\n)*?\*\/|\/\/.*?[\n\r])*(?:"use strict";|'use strict';)?/; | ||
|
|
||
| if (code.match(commentUseStrictRegex)?.[0]) { | ||
| // Add injected code after any comments or "use strict" at the beginning of the bundle. | ||
| ms.replace(commentUseStrictRegex, (match) => `${match}${codeToInject}`); | ||
| } else { | ||
| // ms.replace() doesn't work when there is an empty string match (which happens if | ||
| // there is neither, a comment, nor a "use strict" at the top of the chunk) so we | ||
| // need this special case here. | ||
| ms.prepend(codeToInject); | ||
| } | ||
|
|
||
| return { | ||
| code: ms.toString(), | ||
| map: ms.generateMap(), | ||
| }; | ||
| } | ||
|
|
||
| export async function prepareBundleForDebugIdUpload( | ||
| bundleFilePath: string, | ||
| uploadFolder: string, | ||
| uniqueUploadName: string, | ||
| logger: Logger | ||
| ) { | ||
| let bundleContent; | ||
| try { | ||
| bundleContent = await util.promisify(fs.readFile)(bundleFilePath, "utf8"); | ||
| } catch (e) { | ||
| logger.warn(`Could not read bundle to determine debug ID and source map: ${bundleFilePath}`); | ||
| return; | ||
| } | ||
|
|
||
| const debugId = determineDebugIdFromBundleSource(bundleContent); | ||
| if (debugId === undefined) { | ||
| logger.warn(`Could not determine debug ID from bundle: ${bundleFilePath}`); | ||
| return; | ||
| } | ||
|
|
||
| bundleContent += `\n//# debugId=${debugId}`; | ||
| const writeSourceFilePromise = util.promisify(fs.writeFile)( | ||
| path.join(uploadFolder, `${uniqueUploadName}.js`), | ||
| bundleContent, | ||
| "utf-8" | ||
| ); | ||
|
|
||
| const writeSourceMapFilePromise = determineSourceMapPathFromBundle( | ||
| bundleFilePath, | ||
| bundleContent, | ||
| logger | ||
| ).then(async (sourceMapPath): Promise<void> => { | ||
| if (sourceMapPath) { | ||
| return await prepareSourceMapForDebugIdUpload( | ||
| sourceMapPath, | ||
| path.join(uploadFolder, `${uniqueUploadName}.js.map`), | ||
| debugId, | ||
| logger | ||
| ); | ||
| } | ||
| }); | ||
|
|
||
| await writeSourceFilePromise; | ||
| await writeSourceMapFilePromise; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. l: Why double await here? If we want it to run at the same time, can we just do
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we want to throw here in case one of the promises fails so I went with |
||
| } | ||
|
|
||
| function determineDebugIdFromBundleSource(code: string): string | undefined { | ||
| const match = code.match( | ||
| /sdbid-([0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12})/ | ||
| ); | ||
|
|
||
| if (match) { | ||
| return match[1]; | ||
| } else { | ||
| return undefined; | ||
| } | ||
| } | ||
|
|
||
| async function determineSourceMapPathFromBundle( | ||
| bundlePath: string, | ||
| bundleSource: string, | ||
| logger: Logger | ||
| ): Promise<string | undefined> { | ||
| // 1. try to find source map at `sourceMappingURL` location | ||
| const sourceMappingUrlMatch = bundleSource.match(/^\/\/# sourceMappingURL=(.*)$/); | ||
| if (sourceMappingUrlMatch) { | ||
| const sourceMappingUrl = path.normalize(sourceMappingUrlMatch[1] as string); | ||
| if (path.isAbsolute(sourceMappingUrl)) { | ||
| return sourceMappingUrl; | ||
| } else { | ||
| return path.join(path.dirname(bundlePath), sourceMappingUrl); | ||
| } | ||
| } | ||
|
|
||
| // 2. try to find source map at path adjacent to chunk source, but with `.map` appended | ||
| try { | ||
| const adjacentSourceMapFilePath = bundlePath + ".map"; | ||
| await util.promisify(fs.access)(adjacentSourceMapFilePath); | ||
| return adjacentSourceMapFilePath; | ||
| } catch (e) { | ||
| // noop | ||
| } | ||
|
|
||
| logger.warn(`Could not determine source map path for bundle: ${bundlePath}`); | ||
| return undefined; | ||
| } | ||
|
|
||
| async function prepareSourceMapForDebugIdUpload( | ||
| sourceMapPath: string, | ||
| targetPath: string, | ||
| degugId: string, | ||
|
lforst marked this conversation as resolved.
Outdated
|
||
| logger: Logger | ||
| ): Promise<void> { | ||
| try { | ||
| const sourceMapFileContent = await util.promisify(fs.readFile)(sourceMapPath, { | ||
| encoding: "utf8", | ||
| }); | ||
|
|
||
| const map = JSON.parse(sourceMapFileContent) as Record<string, string>; | ||
|
|
||
| // For now we write both fields until we know what will become the standard - if ever. | ||
| map["debug_id"] = degugId; | ||
| map["debugId"] = degugId; | ||
|
|
||
| await util.promisify(fs.writeFile)(targetPath, JSON.stringify(map), { | ||
| encoding: "utf8", | ||
| }); | ||
| } catch (e) { | ||
| logger.warn(`Failed to prepare source map for debug ID upload: ${sourceMapPath}`); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -214,17 +214,32 @@ export type Options = Omit<IncludeEntry, "paths"> & { | |
| uploadSourceMaps?: boolean; | ||
|
|
||
| /** | ||
| * These options are considered experimental and subject to change. | ||
| * | ||
| * _experiments.injectBuildInformation: | ||
| * If set to true, the plugin will inject an additional `SENTRY_BUILD_INFO` variable. | ||
| * This contains information about the build, e.g. dependencies, node version and other useful data. | ||
| * | ||
| * Defaults to `false`. | ||
| * @hidden | ||
| * Options that are considered experimental and subject to change. | ||
| */ | ||
| _experiments?: { | ||
| /** | ||
| * If set to true, the plugin will inject an additional `SENTRY_BUILD_INFO` variable. | ||
| * This contains information about the build, e.g. dependencies, node version and other useful data. | ||
| * | ||
| * Defaults to `false`. | ||
| */ | ||
| injectBuildInformation?: boolean; | ||
|
|
||
| /** | ||
| * Configuration for debug ID upload. | ||
| * | ||
| * Note: Currently only functional for Vite and Rollup. | ||
| */ | ||
| debugIdUpload?: { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. m: Can we add the default values for include/ignore?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am not sure if a default for include makes sense, since you either define paths or you don't. Ignore makes sense though, so I added it: a7cf4e3 |
||
| /** | ||
| * Glob paths to files that should get be injected with a debug ID and uploaded. | ||
| */ | ||
| include: string | string[]; | ||
| /** | ||
| * Glob paths to files that should be ignored for debug ID injection and upload. | ||
| */ | ||
| ignore?: string | string[]; | ||
| }; | ||
| }; | ||
| }; | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can build this whenever we need to change it - same approach as we use here: getsentry/sentry#46780. I don't think we need to automate this, we can just manually run terser.
So everytime this snippet changes, we rebuild the minified JS. We can have this in follow up PR.