-
Notifications
You must be signed in to change notification settings - Fork 38
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
feat(core): Add experimental debug ID based source map upload to Rollup and Vite plugins #192
Changes from all 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,162 @@ | ||
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="sentry-dbid-__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 | ||
); | ||
} | ||
}); | ||
|
||
return Promise.all([writeSourceFilePromise, writeSourceMapFilePromise]); | ||
} | ||
|
||
/** | ||
* Looks for a particular string pattern (`sdbid-[debug ID]`) in the bundle | ||
* source and extracts the bundle's debug ID from it. | ||
* | ||
* The string pattern is injected via the debug ID injection snipped. | ||
*/ | ||
function determineDebugIdFromBundleSource(code: string): string | undefined { | ||
const match = code.match( | ||
/sentry-dbid-([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; | ||
} | ||
} | ||
|
||
/** | ||
* Applies a set of heuristics to find the source map for a particular bundle. | ||
* | ||
* @returns the path to the bundle's source map or `undefined` if none could be found. | ||
*/ | ||
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; | ||
} | ||
|
||
/** | ||
* Reads a source map, injects debug ID fields, and writes the source map to the target path. | ||
*/ | ||
async function prepareSourceMapForDebugIdUpload( | ||
sourceMapPath: string, | ||
targetPath: string, | ||
debugId: string, | ||
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"] = debugId; | ||
map["debugId"] = debugId; | ||
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. |
||
|
||
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,34 @@ 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?: { | ||
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? 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. | ||
* | ||
* Default: `[]` | ||
*/ | ||
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.
Why did we chose
sdbid
?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.
Just wanted to use something short. It can be changed at any time since nothing depends on this. Do you think another pattern fits better?
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.
I would rather it be
sentry
, but it's no big deal.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.
No! good point! :) befc007