Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions packages/bundler-plugins/src/core/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,11 @@ export function determineReleaseName(): string | undefined {
);
}

function stringifyForCodeInjection(value: unknown): string | undefined {
// Bundles may be inlined in HTML, where JSON quoting does not escape script tags or HTML comments.
return JSON.stringify(value)?.replace(/</g, '\\u003c');
}

/**
* Generates code for the global injector which is responsible for setting the global
* `SENTRY_RELEASE` & `SENTRY_BUILD_INFO` variables.
Expand All @@ -319,12 +324,12 @@ export function generateReleaseInjectorCode({
release: string;
injectBuildInformation: boolean;
}): CodeInjection {
let code = `e.SENTRY_RELEASE={id:${JSON.stringify(release)}};`;
let code = `e.SENTRY_RELEASE={id:${stringifyForCodeInjection(release)}};`;

if (injectBuildInformation) {
const buildInfo = getBuildInformation();

code += `e.SENTRY_BUILD_INFO=${JSON.stringify(buildInfo)};`;
code += `e.SENTRY_BUILD_INFO=${stringifyForCodeInjection(buildInfo)};`;
}

return new CodeInjection(code);
Expand All @@ -335,7 +340,7 @@ export function generateModuleMetadataInjectorCode(metadata: any): CodeInjection
// We are merging the metadata objects in case modules are bundled twice with the plugin
// Use try-catch to avoid issues when bundlers rename global variables like 'window' to 'k'
return new CodeInjection(
`e._sentryModuleMetadata=e._sentryModuleMetadata||{},e._sentryModuleMetadata[(new e.Error).stack]=function(e){for(var n=1;n<arguments.length;n++){var a=arguments[n];if(null!=a)for(var t in a)a.hasOwnProperty(t)&&(e[t]=a[t])}return e}({},e._sentryModuleMetadata[(new e.Error).stack],${JSON.stringify(
`e._sentryModuleMetadata=e._sentryModuleMetadata||{},e._sentryModuleMetadata[(new e.Error).stack]=function(e){for(var n=1;n<arguments.length;n++){var a=arguments[n];if(null!=a)for(var t in a)a.hasOwnProperty(t)&&(e[t]=a[t])}return e}({},e._sentryModuleMetadata[(new e.Error).stack],${stringifyForCodeInjection(
metadata,
)});`,
);
Expand Down
47 changes: 46 additions & 1 deletion packages/bundler-plugins/test/core/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ import {

import childProcess from 'child_process';
import fs from 'fs';
import { describe, it, expect, test, vi } from 'vitest';
import { afterEach, describe, it, expect, test, vi } from 'vitest';
import path from 'node:path';
import { runInNewContext } from 'node:vm';
import { JSDOM } from 'jsdom';

type PackageJson = Record<string, unknown>;

Expand Down Expand Up @@ -284,6 +286,49 @@ describe('generateModuleMetadataInjectorCode', () => {
});
});

describe('code injection in HTML', () => {
afterEach(() => {
vi.restoreAllMocks();
});

it.each([
'</script><script>globalThis.injected = true</script>',
'</ScRiPt \n><script>globalThis.injected = true</script>',
'<!--<script>',
'quotes "\' and backslashes \\u003c with whitespace \n\r\t\u2028\u2029 and <>&',
])('preserves injected values containing %j inside one script element', value => {
vi.spyOn(fs, 'readFileSync').mockReturnValueOnce(
JSON.stringify({ name: 'test-app', dependencies: { [value]: '1.0.0' } }),
);
const metadata = { [value]: { values: [value] } };
const injection = generateReleaseInjectorCode({ release: value, injectBuildInformation: true });
injection.append(generateModuleMetadataInjectorCode(metadata));
const code = injection.code();
const context: {
SENTRY_RELEASE?: { id: string };
SENTRY_BUILD_INFO?: { deps: string[]; depsVersions: Record<string, number>; nodeVersion: number };
_sentryModuleMetadata?: Record<string, unknown>;
} = {};

const dom = new JSDOM(`<script>${code}</script>`);
const scripts = dom.window.document.querySelectorAll('script');
dom.window.close();

expect(scripts).toHaveLength(1);
expect(scripts[0]?.textContent).toBe(code);

runInNewContext(code, context);

expect(context.SENTRY_RELEASE).toEqual({ id: value });
expect(context.SENTRY_BUILD_INFO).toEqual({
deps: [value],
depsVersions: {},
nodeVersion: Number(process.versions.node.split('.')[0]),
});
expect(Object.values(context._sentryModuleMetadata ?? {})).toEqual([metadata]);
});
});

describe('determineReleaseName', () => {
it('runs `git rev-parse HEAD` with windowsHide so no console window flashes on Windows', () => {
// Clear env so the function falls through the CI/git-provider checks to the
Expand Down
Loading