Skip to content

Commit 66f7971

Browse files
author
Luca Forstner
authored
ref(core): Extract debug ID injection into separate plugins (#230)
1 parent d868960 commit 66f7971

File tree

12 files changed

+187
-186
lines changed

12 files changed

+187
-186
lines changed

packages/bundler-plugin-core/package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,7 @@
5757
"find-up": "5.0.0",
5858
"glob": "9.3.2",
5959
"magic-string": "0.27.0",
60-
"unplugin": "1.0.1",
61-
"webpack-sources": "3.2.3"
60+
"unplugin": "1.0.1"
6261
},
6362
"devDependencies": {
6463
"@babel/core": "7.18.5",

packages/bundler-plugin-core/src/debug-id.ts

Lines changed: 0 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,7 @@
11
import * as fs from "fs";
2-
import MagicString from "magic-string";
32
import * as path from "path";
43
import * as util from "util";
54
import { Logger } from "./sentry/logger";
6-
import { stringToUUID } from "./utils";
7-
8-
// TODO: Find a more elaborate process to generate this. (Maybe with type checking and built-in minification)
9-
const DEBUG_ID_INJECTOR_SNIPPET =
10-
';!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){}}();';
11-
12-
export function injectDebugIdSnippetIntoChunk(code: string, filename?: string) {
13-
const debugId = stringToUUID(code); // generate a deterministic debug ID
14-
const ms = new MagicString(code, { filename });
15-
16-
const codeToInject = DEBUG_ID_INJECTOR_SNIPPET.replace(/__SENTRY_DEBUG_ID__/g, debugId);
17-
18-
// We need to be careful not to inject the snippet before any `"use strict";`s.
19-
// As an additional complication `"use strict";`s may come after any number of comments.
20-
const commentUseStrictRegex =
21-
/^(?:\s*|\/\*(.|\r|\n)*?\*\/|\/\/.*?[\n\r])*(?:"use strict";|'use strict';)?/;
22-
23-
if (code.match(commentUseStrictRegex)?.[0]) {
24-
// Add injected code after any comments or "use strict" at the beginning of the bundle.
25-
ms.replace(commentUseStrictRegex, (match) => `${match}${codeToInject}`);
26-
} else {
27-
// ms.replace() doesn't work when there is an empty string match (which happens if
28-
// there is neither, a comment, nor a "use strict" at the top of the chunk) so we
29-
// need this special case here.
30-
ms.prepend(codeToInject);
31-
}
32-
33-
return {
34-
code: ms.toString(),
35-
map: ms.generateMap(),
36-
};
37-
}
385

396
export async function prepareBundleForDebugIdUpload(
407
bundleFilePath: string,

packages/bundler-plugin-core/src/index.ts

Lines changed: 72 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,33 @@
1+
import SentryCli from "@sentry/cli";
2+
import { makeMain } from "@sentry/node";
3+
import { Span, Transaction } from "@sentry/types";
4+
import fs from "fs";
5+
import { glob } from "glob";
6+
import MagicString from "magic-string";
7+
import os from "os";
8+
import path from "path";
19
import { createUnplugin, UnpluginOptions } from "unplugin";
2-
import { Options, BuildContext } from "./types";
10+
import { promisify } from "util";
11+
import { prepareBundleForDebugIdUpload } from "./debug-id";
12+
import { NormalizedOptions, normalizeUserOptions, validateOptions } from "./options-mapping";
13+
import { getSentryCli } from "./sentry/cli";
14+
import { createLogger, Logger } from "./sentry/logger";
315
import {
4-
createNewRelease,
5-
cleanArtifacts,
616
addDeploy,
17+
cleanArtifacts,
18+
createNewRelease,
719
finalizeRelease,
820
setCommits,
9-
uploadSourceMaps,
1021
uploadDebugIdSourcemaps,
22+
uploadSourceMaps,
1123
} from "./sentry/releasePipeline";
12-
import SentryCli from "@sentry/cli";
1324
import {
1425
addPluginOptionInformationToHub,
1526
addSpanToTransaction,
1627
makeSentryClient,
1728
shouldSendTelemetry,
1829
} from "./sentry/telemetry";
19-
import { Span, Transaction } from "@sentry/types";
20-
import { createLogger, Logger } from "./sentry/logger";
21-
import { NormalizedOptions, normalizeUserOptions, validateOptions } from "./options-mapping";
22-
import { getSentryCli } from "./sentry/cli";
23-
import { makeMain } from "@sentry/node";
24-
import os from "os";
25-
import path from "path";
26-
import fs from "fs";
27-
import { createRequire } from "module";
28-
import { promisify } from "util";
30+
import { BuildContext, Options } from "./types";
2931
import {
3032
determineReleaseName,
3133
generateGlobalInjectorCode,
@@ -34,22 +36,10 @@ import {
3436
parseMajorVersion,
3537
stringToUUID,
3638
} from "./utils";
37-
import { glob } from "glob";
38-
import { injectDebugIdSnippetIntoChunk, prepareBundleForDebugIdUpload } from "./debug-id";
39-
import webpackSources from "webpack-sources";
40-
import type { sources } from "webpack";
41-
import type { Plugin } from "rollup";
42-
import MagicString from "magic-string";
43-
44-
// Use createRequire because esm doesn't like built-in require.resolve
45-
const require = createRequire(import.meta.url);
46-
47-
const esbuildDebugIdInjectionFilePath = require.resolve(
48-
"@sentry/bundler-plugin-core/sentry-esbuild-debugid-injection-file"
49-
);
5039

5140
interface SentryUnpluginFactoryOptions {
5241
releaseInjectionPlugin: (injectionCode: string) => UnpluginOptions;
42+
debugIdInjectionPlugin: () => UnpluginOptions;
5343
}
5444
/**
5545
* The sentry bundler plugin concerns itself with two things:
@@ -78,7 +68,10 @@ interface SentryUnpluginFactoryOptions {
7868
*
7969
* This release creation pipeline relies on Sentry CLI to execute the different steps.
8070
*/
81-
export function sentryUnpluginFactory({ releaseInjectionPlugin }: SentryUnpluginFactoryOptions) {
71+
export function sentryUnpluginFactory({
72+
releaseInjectionPlugin,
73+
debugIdInjectionPlugin,
74+
}: SentryUnpluginFactoryOptions) {
8275
return createUnplugin<Options, true>((userOptions, unpluginMetaContext) => {
8376
const options = normalizeUserOptions(userOptions);
8477

@@ -279,108 +272,8 @@ export function sentryUnpluginFactory({ releaseInjectionPlugin }: SentryUnplugin
279272
level: "info",
280273
});
281274
},
282-
rollup: {
283-
renderChunk(code, chunk) {
284-
if (
285-
options.sourcemaps?.assets &&
286-
[".js", ".mjs", ".cjs"].some((ending) => chunk.fileName.endsWith(ending)) // chunks could be any file (html, md, ...)
287-
) {
288-
return injectDebugIdSnippetIntoChunk(code);
289-
} else {
290-
return null; // returning null means not modifying the chunk at all
291-
}
292-
},
293-
},
294-
vite: {
295-
renderChunk(code, chunk) {
296-
if (
297-
options.sourcemaps?.assets &&
298-
[".js", ".mjs", ".cjs"].some((ending) => chunk.fileName.endsWith(ending)) // chunks could be any file (html, md, ...)
299-
) {
300-
return injectDebugIdSnippetIntoChunk(code);
301-
} else {
302-
return null; // returning null means not modifying the chunk at all
303-
}
304-
},
305-
},
306-
webpack(compiler) {
307-
if (options.sourcemaps?.assets) {
308-
// Cache inspired by https://github.com/webpack/webpack/pull/15454
309-
const cache = new WeakMap<sources.Source, sources.Source>();
310-
311-
compiler.hooks.compilation.tap("sentry-plugin", (compilation) => {
312-
compilation.hooks.optimizeChunkAssets.tap("sentry-plugin", (chunks) => {
313-
chunks.forEach((chunk) => {
314-
const fileNames = chunk.files;
315-
fileNames.forEach((fileName) => {
316-
const source = compilation.assets[fileName];
317-
318-
if (!source) {
319-
logger.warn(
320-
"Unable to access compilation assets. If you see this warning, it is likely a bug in the Sentry webpack plugin. Feel free to open an issue at https://github.com/getsentry/sentry-javascript-bundler-plugins with reproduction steps."
321-
);
322-
return;
323-
}
324-
325-
compilation.updateAsset(fileName, (oldSource) => {
326-
const cached = cache.get(oldSource);
327-
if (cached) {
328-
return cached;
329-
}
330-
331-
const originalCode = source.source().toString();
332-
333-
// The source map type is very annoying :(
334-
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any
335-
const originalSourceMap = source.map() as any;
336-
337-
const { code: newCode, map: newSourceMap } = injectDebugIdSnippetIntoChunk(
338-
originalCode,
339-
fileName
340-
);
341-
342-
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
343-
newSourceMap.sources = originalSourceMap.sources as string[];
344-
345-
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
346-
newSourceMap.sourcesContent = originalSourceMap.sourcesContent as string[];
347-
348-
const newSource = new webpackSources.SourceMapSource(
349-
newCode,
350-
fileName,
351-
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
352-
originalSourceMap,
353-
originalCode,
354-
newSourceMap,
355-
false
356-
) as sources.Source;
357-
358-
cache.set(oldSource, newSource);
359-
360-
return newSource;
361-
});
362-
});
363-
});
364-
});
365-
});
366-
}
367-
},
368275
});
369276

370-
if (unpluginMetaContext.framework === "esbuild") {
371-
if (options.sourcemaps?.assets) {
372-
plugins.push({
373-
name: "sentry-esbuild-debug-id-plugin",
374-
esbuild: {
375-
setup({ initialOptions }) {
376-
initialOptions.inject = initialOptions.inject || [];
377-
initialOptions.inject.push(esbuildDebugIdInjectionFilePath);
378-
},
379-
},
380-
});
381-
}
382-
}
383-
384277
if (options.injectRelease && releaseName) {
385278
const injectionCode = generateGlobalInjectorCode({
386279
release: releaseName,
@@ -393,6 +286,10 @@ export function sentryUnpluginFactory({ releaseInjectionPlugin }: SentryUnplugin
393286
plugins.push(releaseInjectionPlugin(injectionCode));
394287
}
395288

289+
if (options.sourcemaps?.assets) {
290+
plugins.push(debugIdInjectionPlugin());
291+
}
292+
396293
return plugins;
397294
});
398295
}
@@ -442,13 +339,11 @@ export function sentryCliBinaryExists(): boolean {
442339
return fs.existsSync(SentryCli.getPath());
443340
}
444341

445-
export function createRollupReleaseInjectionHooks(
446-
injectionCode: string
447-
): Pick<Plugin, "resolveId" | "load" | "transform"> {
342+
export function createRollupReleaseInjectionHooks(injectionCode: string) {
448343
const virtualReleaseInjectionFileId = "\0sentry-release-injection-file";
449344

450345
return {
451-
resolveId(id) {
346+
resolveId(id: string) {
452347
if (id === virtualReleaseInjectionFileId) {
453348
return {
454349
id: virtualReleaseInjectionFileId,
@@ -460,15 +355,15 @@ export function createRollupReleaseInjectionHooks(
460355
}
461356
},
462357

463-
load(id) {
358+
load(id: string) {
464359
if (id === virtualReleaseInjectionFileId) {
465360
return injectionCode;
466361
} else {
467362
return null;
468363
}
469364
},
470365

471-
transform(code, id) {
366+
transform(code: string, id: string) {
472367
if (id === virtualReleaseInjectionFileId) {
473368
return null;
474369
}
@@ -495,4 +390,46 @@ export function createRollupReleaseInjectionHooks(
495390
};
496391
}
497392

393+
export function createRollupDebugIdInjectionHooks() {
394+
return {
395+
renderChunk(code: string, chunk: { fileName: string }) {
396+
if (
397+
[".js", ".mjs", ".cjs"].some((ending) => chunk.fileName.endsWith(ending)) // chunks could be any file (html, md, ...)
398+
) {
399+
const debugId = stringToUUID(code); // generate a deterministic debug ID
400+
const codeToInject = getDebugIdSnippet(debugId);
401+
402+
const ms = new MagicString(code, { filename: chunk.fileName });
403+
404+
// We need to be careful not to inject the snippet before any `"use strict";`s.
405+
// As an additional complication `"use strict";`s may come after any number of comments.
406+
const commentUseStrictRegex =
407+
// Note: CodeQL complains that this regex potentially has n^2 runtime. This likely won't affect realistic files.
408+
/^(?:\s*|\/\*(?:.|\r|\n)*\*\/|\/\/.*[\n\r])*(?:"[^"]*";|'[^']*';)?/;
409+
410+
if (code.match(commentUseStrictRegex)?.[0]) {
411+
// Add injected code after any comments or "use strict" at the beginning of the bundle.
412+
ms.replace(commentUseStrictRegex, (match) => `${match}${codeToInject}`);
413+
} else {
414+
// ms.replace() doesn't work when there is an empty string match (which happens if
415+
// there is neither, a comment, nor a "use strict" at the top of the chunk) so we
416+
// need this special case here.
417+
ms.prepend(codeToInject);
418+
}
419+
420+
return {
421+
code: ms.toString(),
422+
map: ms.generateMap({ file: chunk.fileName }),
423+
};
424+
} else {
425+
return null; // returning null means not modifying the chunk at all
426+
}
427+
},
428+
};
429+
}
430+
431+
export function getDebugIdSnippet(debugId: string): string {
432+
return `;!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]="${debugId}",e._sentryDebugIdIdentifier="sentry-dbid-${debugId}")}catch(e){}}();`;
433+
}
434+
498435
export type { Options } from "./types";

packages/esbuild-plugin/jest.config.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,7 @@ module.exports = {
33
transform: {
44
"^.+\\.(t|j)sx?$": ["@swc/jest"],
55
},
6+
moduleNameMapper: {
7+
uuid: require.resolve("uuid"), // https://stackoverflow.com/a/73203803
8+
},
69
};

packages/esbuild-plugin/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,9 @@
4747
"lint": "eslint ./src ./test"
4848
},
4949
"dependencies": {
50+
"@sentry/bundler-plugin-core": "0.7.2",
5051
"unplugin": "1.0.1",
51-
"@sentry/bundler-plugin-core": "0.7.2"
52+
"uuid": "^9.0.0"
5253
},
5354
"devDependencies": {
5455
"@babel/core": "7.18.5",
@@ -62,6 +63,7 @@
6263
"@swc/jest": "^0.2.21",
6364
"@types/jest": "^28.1.3",
6465
"@types/node": "^18.6.3",
66+
"@types/uuid": "^9.0.1",
6567
"eslint": "^8.18.0",
6668
"jest": "^28.1.1",
6769
"rimraf": "^3.0.2",

0 commit comments

Comments
 (0)