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
59 changes: 43 additions & 16 deletions packages/react-router/src/vite/buildEnd/handleOnBuildEnd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,20 @@ function getSentryConfig(viteConfig: unknown): SentryReactRouterBuildOptions {
return (viteConfig as { sentryConfig: SentryReactRouterBuildOptions }).sentryConfig;
}

/**
* This hook is the only place that injects debug IDs and uploads source maps for React
* Router, so `disable` has to be honoured wherever the user set it. Reading it from the
* top-level config only would silently ignore `unstable_sentryVitePluginOptions`.
*/
function resolveSourceMapsDisable(sentryConfig: SentryReactRouterBuildOptions): boolean | 'disable-upload' | undefined {
// eslint-disable-next-line typescript/no-deprecated
if (sentryConfig.sourceMapsUploadOptions?.enabled === false) {
return true;
}

return sentryConfig.sourcemaps?.disable ?? sentryConfig.unstable_sentryVitePluginOptions?.sourcemaps?.disable;
}
Comment thread
chargome marked this conversation as resolved.

/**
* A build end hook that handles Sentry release creation and source map uploads.
* It creates a new Sentry release if configured, uploads source maps to Sentry,
Expand Down Expand Up @@ -48,8 +62,7 @@ export const sentryOnBuildEnd: BuildEndHook = async ({ reactRouterConfig, viteCo
...unstableSentryVitePluginOptions?.sourcemaps,
...sentryConfig.sourcemaps,
...sourceMapsUploadOptions,
// eslint-disable-next-line typescript/no-deprecated
disable: sourceMapsUploadOptions?.enabled === false ? true : sentryConfig.sourcemaps?.disable,
disable: resolveSourceMapsDisable(sentryConfig),
},
release: {
...unstableSentryVitePluginOptions?.release,
Expand Down Expand Up @@ -80,7 +93,12 @@ export const sentryOnBuildEnd: BuildEndHook = async ({ reactRouterConfig, viteCo
}
}

if (!sourcemaps?.disable && viteConfig.build.sourcemap !== false) {
// `disable: 'disable-upload'` still injects debug IDs, so that source maps can be
// uploaded manually at a later point - only `true` turns source maps off entirely.
const sourceMapsFullyDisabled = sourcemaps?.disable === true;
const uploadDisabled = sourceMapsFullyDisabled || sourcemaps?.disable === 'disable-upload';

if (!sourceMapsFullyDisabled && viteConfig.build.sourcemap !== false) {
// inject debugIds
try {
await cliInstance.execute(
Expand All @@ -92,21 +110,30 @@ export const sentryOnBuildEnd: BuildEndHook = async ({ reactRouterConfig, viteCo
console.error('[Sentry] Could not inject debug ids', error);
}

// upload sourcemaps
try {
await cliInstance.releases.uploadSourceMaps(release?.name || 'undefined', {
include: [
{
paths: [reactRouterConfig.buildDirectory],
},
],
live: 'rejectOnError',
});
} catch (error) {
// eslint-disable-next-line no-console
console.error('[Sentry] Could not upload sourcemaps', error);
if (!uploadDisabled) {
// upload sourcemaps
try {
await cliInstance.releases.uploadSourceMaps(release?.name || 'undefined', {
include: [
{
paths: [reactRouterConfig.buildDirectory],
},
],
live: 'rejectOnError',
});
} catch (error) {
// eslint-disable-next-line no-console
console.error('[Sentry] Could not upload sourcemaps', error);
}
}
}

// Only clean up source maps that were actually uploaded. Deleting them after skipping
// the upload would leave the user with neither, breaking a manual upload.
if (uploadDisabled) {
return;
}

// delete sourcemaps after upload
let updatedFilesToDeleteAfterUpload = await sourcemaps?.filesToDeleteAfterUpload;

Expand Down
37 changes: 31 additions & 6 deletions packages/react-router/src/vite/makeCustomSentryVitePlugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,20 @@ export async function makeCustomSentryVitePlugins(options: SentryReactRouterBuil
release,
} = options;

const unstableSourcemapsDisable = unstable_sentryVitePluginOptions?.sourcemaps?.disable;

// Anything other than `true` would have the Vite plugin inject debug IDs on top of the
// ones `sentryOnBuildEnd` injects, which breaks source map resolution. The value still
// applies to the buildEnd hook - only the Vite plugin ignores it.
if (unstableSourcemapsDisable !== undefined && unstableSourcemapsDisable !== true) {
// eslint-disable-next-line no-console
console.warn(
`[Sentry] \`unstable_sentryVitePluginOptions.sourcemaps.disable: ${JSON.stringify(
unstableSourcemapsDisable,
)}\` does not apply to the Vite plugin. Debug ID injection and source map upload are handled by the \`sentryOnBuildEnd\` hook for React Router, so letting the Vite plugin do it as well would inject a second debug ID per chunk. The option still applies to \`sentryOnBuildEnd\`.`,
);
Comment thread
chargome marked this conversation as resolved.
}

const sentryVitePlugins = sentryVitePlugin({
applicationKey,
authToken: authToken ?? process.env.SENTRY_AUTH_TOKEN,
Expand All @@ -27,27 +41,38 @@ export async function makeCustomSentryVitePlugins(options: SentryReactRouterBuil
org: org ?? process.env.SENTRY_ORG,
project: project ?? process.env.SENTRY_PROJECT,
telemetry: telemetry ?? true,
// Spread here so it can override the plain options above, but not the objects
// merged below - object spread replaces whole keys rather than deep-merging.
...unstable_sentryVitePluginOptions,
_metaOptions: {
...unstable_sentryVitePluginOptions?._metaOptions,
telemetry: {
...unstable_sentryVitePluginOptions?._metaOptions?.telemetry,
metaFramework: 'react-router',
},
...unstable_sentryVitePluginOptions?._metaOptions,
},
reactComponentAnnotation: {
enabled: reactComponentAnnotation?.enabled ?? undefined,
ignoredComponents: reactComponentAnnotation?.ignoredComponents ?? undefined,
// Only assign when set, as an explicit `undefined` would erase the unstable value
...(reactComponentAnnotation?.enabled !== undefined && { enabled: reactComponentAnnotation.enabled }),
...(reactComponentAnnotation?.ignoredComponents !== undefined && {
ignoredComponents: reactComponentAnnotation.ignoredComponents,
}),
...unstable_sentryVitePluginOptions?.reactComponentAnnotation,
},
release: {
...unstable_sentryVitePluginOptions?.release,
...release,
},
// will be handled in buildEnd hook
sourcemaps: {
disable: true,
...unstable_sentryVitePluginOptions?.sourcemaps,
// Injection and upload are handled in the buildEnd hook, so the Vite plugin must
// never do it too. This is deliberately not overridable - see the warning above.
disable: true,
Comment thread
chargome marked this conversation as resolved.
// The plugin deletes these in a `finally` block that runs regardless of `disable`,
// which would remove the maps before `sentryOnBuildEnd` gets to upload them.
// Deletion is handled there instead, from the same option.
filesToDeleteAfterUpload: undefined,
},
...unstable_sentryVitePluginOptions,
}) as Plugin[];

return sentryVitePlugins;
Expand Down
170 changes: 170 additions & 0 deletions packages/react-router/test/vite/buildEnd/handleOnBuildEnd.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,176 @@ describe('sentryOnBuildEnd', () => {
expect(mockSentryCliInstance.releases.uploadSourceMaps).not.toHaveBeenCalled();
});

it('should not upload source maps when disabled via top-level sourcemaps.disable', async () => {
const config = {
...defaultConfig,
viteConfig: {
...defaultConfig.viteConfig,
sentryConfig: {
...defaultConfig.viteConfig.sentryConfig,
sourcemaps: { disable: true },
},
} as unknown as TestConfig,
};

// @ts-expect-error - mocking the React config
await sentryOnBuildEnd(config);

expect(mockSentryCliInstance.execute).not.toHaveBeenCalled();
expect(mockSentryCliInstance.releases.uploadSourceMaps).not.toHaveBeenCalled();
});

// `disable` used to be read from the top-level config only, so this opt-out was
// silently ignored while the Vite plugin honoured it - see #22929.
it('should not upload source maps when disabled via unstable_sentryVitePluginOptions', async () => {
const config = {
...defaultConfig,
viteConfig: {
...defaultConfig.viteConfig,
sentryConfig: {
...defaultConfig.viteConfig.sentryConfig,
unstable_sentryVitePluginOptions: {
sourcemaps: { disable: true },
},
},
} as unknown as TestConfig,
};

// @ts-expect-error - mocking the React config
await sentryOnBuildEnd(config);

expect(mockSentryCliInstance.execute).not.toHaveBeenCalled();
expect(mockSentryCliInstance.releases.uploadSourceMaps).not.toHaveBeenCalled();
});

it('should let top-level sourcemaps.disable override unstable_sentryVitePluginOptions', async () => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm didn't we say that unstable options always have precedence? Tbh this is logaf-super-L for me since we can remove unstable options with v11 but was curious on your thoughts either way

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I misremembered and it's the other way around

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, you're right actually!

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was pre-existing, I'll streamline in a follow up

const config = {
...defaultConfig,
viteConfig: {
...defaultConfig.viteConfig,
sentryConfig: {
...defaultConfig.viteConfig.sentryConfig,
sourcemaps: { disable: false },
unstable_sentryVitePluginOptions: {
sourcemaps: { disable: true },
},
},
} as unknown as TestConfig,
};

// @ts-expect-error - mocking the React config
await sentryOnBuildEnd(config);

expect(mockSentryCliInstance.releases.uploadSourceMaps).toHaveBeenCalled();
});

it('should still upload source maps when unstable_sentryVitePluginOptions only sets unrelated sourcemaps keys', async () => {
const config = {
...defaultConfig,
viteConfig: {
...defaultConfig.viteConfig,
sentryConfig: {
...defaultConfig.viteConfig.sentryConfig,
unstable_sentryVitePluginOptions: {
sourcemaps: { filesToDeleteAfterUpload: ['./build/**/*.map'] },
},
},
} as unknown as TestConfig,
};

// @ts-expect-error - mocking the React config
await sentryOnBuildEnd(config);

expect(mockSentryCliInstance.execute).toHaveBeenCalledWith(['sourcemaps', 'inject', '/build'], false);
expect(mockSentryCliInstance.releases.uploadSourceMaps).toHaveBeenCalled();
expect(glob).toHaveBeenCalledWith(['./build/**/*.map'], {
absolute: true,
nodir: true,
});
});

// `'disable-upload'` means "inject debug IDs, but let me upload the maps myself", so
// injection must still run and the maps must survive.
it('should inject debug IDs but skip upload and deletion when disable is "disable-upload"', async () => {
const config = {
...defaultConfig,
viteConfig: {
...defaultConfig.viteConfig,
sentryConfig: {
...defaultConfig.viteConfig.sentryConfig,
sourcemaps: { disable: 'disable-upload' },
},
} as unknown as TestConfig,
};

// @ts-expect-error - mocking the React config
await sentryOnBuildEnd(config);

expect(mockSentryCliInstance.execute).toHaveBeenCalledWith(['sourcemaps', 'inject', '/build'], false);
expect(mockSentryCliInstance.releases.uploadSourceMaps).not.toHaveBeenCalled();
expect(glob).not.toHaveBeenCalled();
});

it('should honour "disable-upload" set via unstable_sentryVitePluginOptions', async () => {
const config = {
...defaultConfig,
viteConfig: {
...defaultConfig.viteConfig,
sentryConfig: {
...defaultConfig.viteConfig.sentryConfig,
unstable_sentryVitePluginOptions: {
sourcemaps: { disable: 'disable-upload' },
},
},
} as unknown as TestConfig,
};

// @ts-expect-error - mocking the React config
await sentryOnBuildEnd(config);

expect(mockSentryCliInstance.execute).toHaveBeenCalledWith(['sourcemaps', 'inject', '/build'], false);
expect(mockSentryCliInstance.releases.uploadSourceMaps).not.toHaveBeenCalled();
expect(glob).not.toHaveBeenCalled();
});

// Deleting maps that were never uploaded would leave the user with neither.
it('should not delete source maps when upload is disabled', async () => {
const config = {
...defaultConfig,
viteConfig: {
...defaultConfig.viteConfig,
sentryConfig: {
...defaultConfig.viteConfig.sentryConfig,
sourcemaps: { disable: true },
},
} as unknown as TestConfig,
};

// @ts-expect-error - mocking the React config
await sentryOnBuildEnd(config);

expect(glob).not.toHaveBeenCalled();
expect(fs.promises.rm).not.toHaveBeenCalled();
});

it('should not delete source maps when disabled via the deprecated sourceMapsUploadOptions', async () => {
const config = {
...defaultConfig,
viteConfig: {
...defaultConfig.viteConfig,
sentryConfig: {
...defaultConfig.viteConfig.sentryConfig,
sourceMapsUploadOptions: { enabled: false },
},
} as unknown as TestConfig,
};

// @ts-expect-error - mocking the React config
await sentryOnBuildEnd(config);

expect(glob).not.toHaveBeenCalled();
});

it('should delete source maps after upload with default pattern', async () => {
// @ts-expect-error - mocking the React config
await sentryOnBuildEnd(defaultConfig);
Expand Down
Loading
Loading