Skip to content

Commit 97a2a2f

Browse files
authored
feat: Add _experiments.injectBuildInformation option (#176)
1 parent 5e0710d commit 97a2a2f

File tree

31 files changed

+632
-146
lines changed

31 files changed

+632
-146
lines changed

.prettierignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
packages/e2e-tests/scenarios/*/ref/**/*
1+
packages/e2e-tests/scenarios/*/ref/**/*
2+
packages/bundler-plugin-core/test/fixtures

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
"npm-run-all": "^4.1.5"
3838
},
3939
"volta": {
40-
"node": "14.21.1",
40+
"node": "14.21.2",
4141
"yarn": "1.22.19"
4242
}
4343
}

packages/bundler-plugin-core/.eslintrc.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,13 @@ const jestPackageJson = require("jest/package.json");
44
module.exports = {
55
root: true,
66
extends: ["@sentry-internal/eslint-config/jest", "@sentry-internal/eslint-config/base"],
7-
ignorePatterns: [".eslintrc.js", "dist", "jest.config.js", "rollup.config.js"],
7+
ignorePatterns: [
8+
".eslintrc.js",
9+
"dist",
10+
"jest.config.js",
11+
"rollup.config.js",
12+
"test/fixtures/**/*",
13+
],
814
parserOptions: {
915
tsconfigRootDir: __dirname,
1016
project: ["./src/tsconfig.json", "./test/tsconfig.json"],

packages/bundler-plugin-core/jest.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ const packageJson = require("./package.json");
22

33
module.exports = {
44
testEnvironment: "node",
5+
modulePathIgnorePatterns: ["fixtures"],
56
transform: {
67
"^.+\\.(t|j)sx?$": [
78
"@swc/jest",

packages/bundler-plugin-core/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"@sentry/cli": "^2.10.0",
4545
"@sentry/node": "^7.19.0",
4646
"@sentry/tracing": "^7.19.0",
47+
"find-up": "5.0.0",
4748
"magic-string": "0.27.0",
4849
"unplugin": "1.0.1"
4950
},

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

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { getSentryCli } from "./sentry/cli";
2424
import { makeMain } from "@sentry/node";
2525
import path from "path";
2626
import fs from "fs";
27+
import { getDependencies, getPackageJson, parseMajorVersion } from "./utils";
2728

2829
const ALLOWED_TRANSFORMATION_FILE_ENDINGS = [".js", ".ts", ".jsx", ".tsx", ".mjs"];
2930

@@ -231,6 +232,7 @@ const unplugin = createUnplugin<Options>((options, unpluginMetaContext) => {
231232
generateGlobalInjectorCode({
232233
release: await releaseNamePromise,
233234
injectReleasesMap: internalOptions.injectReleasesMap,
235+
injectBuildInformation: internalOptions._experiments.injectBuildInformation || false,
234236
org: internalOptions.org,
235237
project: internalOptions.project,
236238
})
@@ -328,17 +330,19 @@ function handleError(
328330
}
329331

330332
/**
331-
* Generates code for the "sentry-release-injector" which is responsible for setting the global `SENTRY_RELEASE`
332-
* variable.
333+
* Generates code for the global injector which is responsible for setting the global
334+
* `SENTRY_RELEASE` & `SENTRY_BUILD_INFO` variables.
333335
*/
334336
function generateGlobalInjectorCode({
335337
release,
336338
injectReleasesMap,
339+
injectBuildInformation,
337340
org,
338341
project,
339342
}: {
340343
release: string;
341344
injectReleasesMap: boolean;
345+
injectBuildInformation: boolean;
342346
org?: string;
343347
project?: string;
344348
}) {
@@ -363,9 +367,30 @@ function generateGlobalInjectorCode({
363367
_global.SENTRY_RELEASES["${key}"]={id:"${release}"};`;
364368
}
365369

370+
if (injectBuildInformation) {
371+
const buildInfo = getBuildInformation();
372+
373+
code += `
374+
_global.SENTRY_BUILD_INFO=${JSON.stringify(buildInfo)};`;
375+
}
376+
366377
return code;
367378
}
368379

380+
export function getBuildInformation() {
381+
const packageJson = getPackageJson();
382+
383+
const { deps, depsVersions } = packageJson
384+
? getDependencies(packageJson)
385+
: { deps: [], depsVersions: {} };
386+
387+
return {
388+
deps,
389+
depsVersions,
390+
nodeVersion: parseMajorVersion(process.version),
391+
};
392+
}
393+
369394
/**
370395
* Determines whether the Sentry CLI binary is in its expected location.
371396
* This function is useful since `@sentry/cli` installs the binary via a post-install

packages/bundler-plugin-core/src/options-mapping.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ type RequiredInternalOptions = Required<
1212
| "cleanArtifacts"
1313
| "telemetry"
1414
| "injectReleasesMap"
15+
| "_experiments"
1516
>
1617
>;
1718

@@ -89,6 +90,7 @@ export function normalizeUserOptions(userOptions: UserOptions): InternalOptions
8990
silent: userOptions.silent ?? false,
9091
telemetry: userOptions.telemetry ?? true,
9192
injectReleasesMap: userOptions.injectReleasesMap ?? false,
93+
_experiments: userOptions._experiments ?? {},
9294

9395
// These options and can also be set via env variables or the config file.
9496
// If they're set in the options, we simply pass them to the CLI constructor.

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,20 @@ export type Options = Omit<IncludeEntry, "paths"> & {
198198
* Defaults to `false`.
199199
*/
200200
injectReleasesMap?: boolean;
201+
202+
/**
203+
* These options are considered experimental and subject to change.
204+
*
205+
* _experiments.injectBuildInformation:
206+
* If set to true, the plugin will inject an additional `SENTRY_BUILD_INFO` variable.
207+
* This contains information about the build, e.g. dependencies, node version and other useful data.
208+
*
209+
* Defaults to `false`.
210+
* @hidden
211+
*/
212+
_experiments?: {
213+
injectBuildInformation?: boolean;
214+
};
201215
};
202216

203217
export type IncludeEntry = {

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

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
import findUp from "find-up";
2+
import path from "node:path";
3+
import fs from "node:fs";
4+
import os from "node:os";
5+
16
/**
27
* Checks whether the given input is already an array, and if it isn't, wraps it in one.
38
*
@@ -7,3 +12,156 @@
712
export function arrayify<T = unknown>(maybeArray: T | T[]): T[] {
813
return Array.isArray(maybeArray) ? maybeArray : [maybeArray];
914
}
15+
16+
type PackageJson = Record<string, unknown>;
17+
18+
/**
19+
* Get the closes package.json from a given starting point upwards.
20+
* This handles a few edge cases:
21+
* * Check if a given file package.json appears to be an actual NPM package.json file
22+
* * Stop at the home dir, to avoid looking too deeply
23+
*/
24+
export function getPackageJson({ cwd, stopAt }: { cwd?: string; stopAt?: string } = {}):
25+
| PackageJson
26+
| undefined {
27+
return lookupPackageJson(cwd ?? process.cwd(), path.normalize(stopAt ?? os.homedir()));
28+
}
29+
30+
export function parseMajorVersion(version: string): number | undefined {
31+
// if it has a `v` prefix, remove it
32+
if (version.startsWith("v")) {
33+
version = version.slice(1);
34+
}
35+
36+
// First, try simple lookup of exact, ~ and ^ versions
37+
const regex = /^[\^~]?(\d+)(\.\d+)?(\.\d+)?(-.+)?/;
38+
39+
const match = version.match(regex);
40+
if (match) {
41+
return parseInt(match[1] as string, 10);
42+
}
43+
44+
// Try to parse e.g. 1.x
45+
const coerced = parseInt(version, 10);
46+
if (!Number.isNaN(coerced)) {
47+
return coerced;
48+
}
49+
50+
// Match <= and >= ranges.
51+
const gteLteRegex = /^[<>]=\s*(\d+)(\.\d+)?(\.\d+)?(-.+)?/;
52+
const gteLteMatch = version.match(gteLteRegex);
53+
if (gteLteMatch) {
54+
return parseInt(gteLteMatch[1] as string, 10);
55+
}
56+
57+
// match < ranges
58+
const ltRegex = /^<\s*(\d+)(\.\d+)?(\.\d+)?(-.+)?/;
59+
const ltMatch = version.match(ltRegex);
60+
if (ltMatch) {
61+
// Two scenarios:
62+
// a) < 2.0.0 --> return 1
63+
// b) < 2.1.0 --> return 2
64+
65+
const major = parseInt(ltMatch[1] as string, 10);
66+
67+
if (
68+
// minor version > 0
69+
(typeof ltMatch[2] === "string" && parseInt(ltMatch[2].slice(1), 10) > 0) ||
70+
// patch version > 0
71+
(typeof ltMatch[3] === "string" && parseInt(ltMatch[3].slice(1), 10) > 0)
72+
) {
73+
return major;
74+
}
75+
76+
return major - 1;
77+
}
78+
79+
// match > ranges
80+
const gtRegex = /^>\s*(\d+)(\.\d+)?(\.\d+)?(-.+)?/;
81+
const gtMatch = version.match(gtRegex);
82+
if (gtMatch) {
83+
// We always return the version here, even though it _may_ be incorrect
84+
// E.g. if given > 2.0.0, it should be 2 if there exists any 2.x.x version, else 3
85+
// Since there is no way for us to know this, we're going to assume any kind of patch/feature release probably exists
86+
return parseInt(gtMatch[1] as string, 10);
87+
}
88+
return undefined;
89+
}
90+
91+
// This is an explicit list of packages where we want to include the (major) version number.
92+
const PACKAGES_TO_INCLUDE_VERSION = [
93+
"react",
94+
"@angular/core",
95+
"vue",
96+
"ember-source",
97+
"svelte",
98+
"@sveltejs/kit",
99+
"webpack",
100+
"vite",
101+
"gatsby",
102+
"next",
103+
"remix",
104+
"rollup",
105+
"esbuild",
106+
];
107+
108+
export function getDependencies(packageJson: PackageJson): {
109+
deps: string[];
110+
depsVersions: Record<string, number>;
111+
} {
112+
const dependencies: Record<string, string> = Object.assign(
113+
{},
114+
packageJson["devDependencies"] ?? {},
115+
packageJson["dependencies"] ?? {}
116+
);
117+
118+
const deps = Object.keys(dependencies).sort();
119+
120+
const depsVersions: Record<string, number> = deps.reduce((depsVersions, depName) => {
121+
if (PACKAGES_TO_INCLUDE_VERSION.includes(depName)) {
122+
const version = dependencies[depName] as string;
123+
const majorVersion = parseMajorVersion(version);
124+
if (majorVersion) {
125+
depsVersions[depName] = majorVersion;
126+
}
127+
}
128+
return depsVersions;
129+
}, {} as Record<string, number>);
130+
131+
return { deps, depsVersions };
132+
}
133+
134+
function lookupPackageJson(cwd: string, stopAt: string): PackageJson | undefined {
135+
const jsonPath = findUp.sync(
136+
(dirName) => {
137+
// Stop if we reach this dir
138+
if (path.normalize(dirName) === stopAt) {
139+
return findUp.stop;
140+
}
141+
142+
return findUp.sync.exists(dirName + "/package.json") ? "package.json" : undefined;
143+
},
144+
{ cwd }
145+
);
146+
147+
if (!jsonPath) {
148+
return undefined;
149+
}
150+
151+
try {
152+
const jsonStr = fs.readFileSync(jsonPath, "utf8");
153+
const json = JSON.parse(jsonStr) as PackageJson;
154+
155+
// Ensure it is an actual package.json
156+
// This is very much not bulletproof, but should be good enough
157+
if ("name" in json || "private" in json) {
158+
return json;
159+
}
160+
} catch (error) {
161+
// Ignore and walk up
162+
}
163+
164+
// Continue up the tree, if we find a fitting package.json
165+
const newCwd = path.dirname(path.resolve(jsonPath + "/.."));
166+
return lookupPackageJson(newCwd, stopAt);
167+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
// Placeholder here

0 commit comments

Comments
 (0)