Skip to content
Open
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
27 changes: 27 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,33 @@

Work in this release was contributed by @sebws, @harshit078, and @fedetorre. Thank you for your contributions!

- **feat(tanstackstart-react): Auto-copy instrumentation file to server build output ([#19104](https://github.com/getsentry/sentry-javascript/pull/19104))**

The Sentry TanStack Start Vite plugin now automatically copies the `instrument.server.mjs` file to the correct server build output directory after the build completes. The output directory is auto-detected based on the deployment target (e.g., Nitro), so you no longer need to manually ensure the instrumentation file ends up in the right place. If auto-detection doesn't work for your setup, you can override the file path and output directory manually:

```ts
// vite.config.ts
import { defineConfig } from 'vite';
import { sentryTanstackStart } from '@sentry/tanstackstart-react';
import { tanstackStart } from '@tanstack/react-start/plugin/vite';

export default defineConfig({
plugins: [
tanstackStart(),
sentryTanstackStart({
org: 'your-org',
project: 'your-project',

// Custom path to the instrumentation file (default: 'instrument.server.mjs')
instrumentationFilePath: 'my-instrument.server.mjs',

// Override the auto-detected server output directory
serverOutputDir: 'build/server',
}),
],
});
```

- **feat(core): Introduces a new `Sentry.setConversationId()` API to track multi turn AI conversations across API calls. ([#18909](https://github.com/getsentry/sentry-javascript/pull/18909))**

You can now set a conversation ID that will be automatically applied to spans within that scope. This allows you to link traces from the same conversation together.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"version": "0.0.1",
"type": "module",
"scripts": {
"build": "vite build && cp instrument.server.mjs .output/server",
"build": "vite build",
"start": "node --import ./.output/server/instrument.server.mjs .output/server/index.mjs",
"test": "playwright test",
"clean": "npx rimraf node_modules pnpm-lock.yaml",
Expand Down
104 changes: 104 additions & 0 deletions packages/tanstackstart-react/src/vite/copyInstrumentationFile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import * as fs from 'fs';
import * as path from 'path';
import type { Plugin, ResolvedConfig } from 'vite';

interface CopyInstrumentationFilePluginOptions {
instrumentationFilePath?: string;
serverOutputDir?: string;
}

/**
* Creates a Vite plugin that copies the user's instrumentation file
* to the server build output directory after the build completes.
*
* By default, copies `instrument.server.mjs` from the project root.
* A custom file path can be provided via `instrumentationFilePath`.
*
* The server output directory can be configured via `serverOutputDir`.
* By default, it will be auto-detected based on the vite plugin being used.
*
* For nitro deployments, we use the Nitro Vite environment config to get the server output directory.
* For cloudflare and netlify deployments, we assume the server output directory is `dist/server`, which is the default output directory for these plugins.
*/
export function makeCopyInstrumentationFilePlugin(options?: CopyInstrumentationFilePluginOptions): Plugin {
let serverOutputDir: string | undefined;
type RollupOutputDir = { dir?: string };
type ViteEnvironments = Record<string, { build?: { rollupOptions?: { output?: RollupOutputDir } } }>;

return {
name: 'sentry-tanstackstart-copy-instrumentation-file',
apply: 'build',
enforce: 'post',

configResolved(resolvedConfig: ResolvedConfig) {
// If user provided serverOutputDir, use it directly and skip auto-detection
if (options?.serverOutputDir) {
serverOutputDir = path.resolve(resolvedConfig.root, options.serverOutputDir);
return;
}

const plugins = resolvedConfig.plugins || [];
const hasPlugin = (name: string): boolean => plugins.some(p => p.name?.includes(name));

if (hasPlugin('nitro')) {
// There seems to be no way to access the nitro instance directly to get the server dir, so we need to access it via the vite environment config.
// This works because Nitro's Vite bundler sets the rollup output dir to the resolved serverDir:
// https://github.com/nitrojs/nitro/blob/1954b824597f6ac52fb8b064415cb85d0feda078/src/build/vite/bundler.ts#L35
const environments = (resolvedConfig as { environments?: ViteEnvironments }).environments;
const nitroEnv = environments?.nitro;
if (nitroEnv) {
const rollupOutput = nitroEnv.build?.rollupOptions?.output;
const dir = rollupOutput?.dir;
if (dir) {
serverOutputDir = dir;
}
}
} else if (hasPlugin('cloudflare') || hasPlugin('netlify')) {
// There seems to be no way for users to configure the server output dir for these plugins, so we just assume it's `dist/server`, which is the default output dir.
serverOutputDir = path.resolve(resolvedConfig.root, 'dist', 'server');
} else {
// eslint-disable-next-line no-console
console.warn(
'[Sentry] Could not detect nitro, cloudflare, or netlify vite plugin. ' +
'The instrument.server.mjs file will not be copied to the build output automatically.',
);
}
},

async closeBundle() {
// Auto-detection failed, so we don't copy the instrumentation file.
if (!serverOutputDir) {
return;
}

const instrumentationFileName = options?.instrumentationFilePath || 'instrument.server.mjs';
const instrumentationSource = path.resolve(process.cwd(), instrumentationFileName);
Copy link

Choose a reason for hiding this comment

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

Bug: The source instrumentation file path is resolved using process.cwd(), while the destination uses resolvedConfig.root. This will fail when these paths differ, like in monorepos.
Severity: MEDIUM

Suggested Fix

In the configResolved hook, store resolvedConfig.root in a closure-scoped variable. Then, in the closeBundle hook, use this stored variable instead of process.cwd() to resolve the source instrumentation file's path. This ensures both source and destination paths are resolved relative to the same Vite project root.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: packages/tanstackstart-react/src/vite/copyInstrumentationFile.ts#L75

Potential issue: The plugin resolves the destination directory for the instrumentation
file using `resolvedConfig.root` but resolves the source file path using
`process.cwd()`. This inconsistency causes a problem when Vite is run with a custom root
directory or in a monorepo setup, as `process.cwd()` and `resolvedConfig.root` will
differ. In such cases, the plugin will fail to find the `instrument.server.mjs` file at
the expected location (relative to the project root), causing it to be silently omitted
from the final build, defeating the purpose of the plugin.

Did we get this right? 👍 / 👎 to inform future reviews.


// Check if the instrumentation file exists.
try {
await fs.promises.access(instrumentationSource);
} catch {
// eslint-disable-next-line no-console
console.warn(
`[Sentry] No ${instrumentationFileName} file found in project root. ` +
'The Sentry instrumentation file will not be copied to the build output.',
);
return;
}

// Copy the instrumentation file to the server output directory.
const destinationFileName = path.basename(instrumentationFileName);
const destination = path.resolve(serverOutputDir, destinationFileName);

try {
await fs.promises.mkdir(serverOutputDir, { recursive: true });
await fs.promises.copyFile(instrumentationSource, destination);
// eslint-disable-next-line no-console
console.log(`[Sentry] Copied ${destinationFileName} to ${destination}`);
} catch (error) {
// eslint-disable-next-line no-console
console.warn(`[Sentry] Failed to copy ${destinationFileName} to build output.`, error);
}
},
};
}
32 changes: 32 additions & 0 deletions packages/tanstackstart-react/src/vite/sentryTanstackStart.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { BuildTimeOptionsBase } from '@sentry/core';
import type { Plugin } from 'vite';
import { makeAutoInstrumentMiddlewarePlugin } from './autoInstrumentMiddleware';
import { makeCopyInstrumentationFilePlugin } from './copyInstrumentationFile';
import { makeAddSentryVitePlugin, makeEnableSourceMapsVitePlugin } from './sourceMaps';

/**
Expand All @@ -19,6 +20,29 @@ export interface SentryTanstackStartOptions extends BuildTimeOptionsBase {
* @default true
*/
autoInstrumentMiddleware?: boolean;

/**
* Path to the instrumentation file to be copied to the server build output directory.
*
* Relative paths are resolved from the current working directory.
*
* @default 'instrument.server.mjs'
*/
instrumentationFilePath?: string;

/**
* Custom server output directory path for the instrumentation file.
*
* By default, the plugin auto-detects the output directory:
* - For Nitro: reads from Vite environment config
* - For Cloudflare/Netlify: uses `dist/server`
*
* Use this option to override the default when your deployment target
* uses a non-standard output directory.
*
* @example 'build/server'
*/
serverOutputDir?: string;
}

/**
Expand Down Expand Up @@ -53,6 +77,14 @@ export function sentryTanstackStart(options: SentryTanstackStartOptions = {}): P

const plugins: Plugin[] = [...makeAddSentryVitePlugin(options)];

// copy instrumentation file to build output
plugins.push(
makeCopyInstrumentationFilePlugin({
instrumentationFilePath: options.instrumentationFilePath,
serverOutputDir: options.serverOutputDir,
}),
);

// middleware auto-instrumentation
if (options.autoInstrumentMiddleware !== false) {
plugins.push(makeAutoInstrumentMiddlewarePlugin({ enabled: true, debug: options.debug }));
Expand Down
Loading
Loading