feat(web-tanstack): wire security headers middleware and nonce threading

Register core-shared/security/tanstack server middleware in app.config.ts
as a Nitro/H3 hook that emits the six security headers and forwards the
per-request nonce. Update instrumentation-client to read the nonce from
<meta name="csp-nonce"> and pass it to initSentryClientReact.

Add nonce support to initSentryClientReact (feedbackIntegration receives
styleNonce/scriptNonce), mirroring the initSentryClient pattern already
in place for web-next.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-20 10:33:09 +00:00
parent dc718fd9c8
commit a540f3afb1
6 changed files with 148 additions and 10 deletions

View File

@@ -1,17 +1,22 @@
// packages/core-shared/src/instrumentation/sentry/init-client-react.test.ts
import { describe, it, expect, vi, beforeEach } from "vitest";
const { replayIntegration } = vi.hoisted(() => {
const { replayIntegration, feedbackIntegration } = vi.hoisted(() => {
const replayIntegration = vi.fn((opts: unknown) => ({
name: "Replay",
_opts: opts,
}));
return { replayIntegration };
const feedbackIntegration = vi.fn((opts: unknown) => ({
name: "Feedback",
_opts: opts,
}));
return { replayIntegration, feedbackIntegration };
});
vi.mock("@sentry/react", () => ({
init: vi.fn(),
replayIntegration,
feedbackIntegration,
}));
import * as SentryReact from "@sentry/react";
@@ -59,4 +64,29 @@ describe("initSentryClientReact", () => {
initSentryClientReact({ dsn: "", app: "web-tanstack" });
expect(SentryReact.init).not.toHaveBeenCalled();
});
it("attaches feedbackIntegration when SentryReact.feedbackIntegration is available", () => {
initSentryClientReact({ dsn: "https://x@y/1", app: "web-tanstack" });
expect(feedbackIntegration).toHaveBeenCalledTimes(1);
});
it("passes styleNonce and scriptNonce to feedbackIntegration when nonce provided", () => {
initSentryClientReact({
dsn: "https://x@y/1",
app: "web-tanstack",
nonce: "abc123",
});
const feedbackOpts = (feedbackIntegration as ReturnType<typeof vi.fn>).mock
.calls[0]![0] as Record<string, unknown>;
expect(feedbackOpts["styleNonce"]).toBe("abc123");
expect(feedbackOpts["scriptNonce"]).toBe("abc123");
});
it("omits nonce props from feedbackIntegration when nonce not provided", () => {
initSentryClientReact({ dsn: "https://x@y/1", app: "web-tanstack" });
const feedbackOpts = (feedbackIntegration as ReturnType<typeof vi.fn>).mock
.calls[0]![0] as Record<string, unknown>;
expect(feedbackOpts["styleNonce"]).toBeUndefined();
expect(feedbackOpts["scriptNonce"]).toBeUndefined();
});
});

View File

@@ -72,6 +72,7 @@ export function initSentryClientReact(opts: InitClientOpts): void {
if (!opts.dsn) return;
const isProd = process.env["NODE_ENV"] === "production";
const { nonce } = opts;
const tracesSampleRate =
process.env["SENTRY_TRACES_SAMPLE_RATE"] !== undefined
? Number(process.env["SENTRY_TRACES_SAMPLE_RATE"])
@@ -127,6 +128,13 @@ export function initSentryClientReact(opts: InitClientOpts): void {
maskAllInputs: true,
blockAllMedia: true,
}),
...(SentryReact.feedbackIntegration
? [
SentryReact.feedbackIntegration({
...(nonce ? { styleNonce: nonce, scriptNonce: nonce } : {}),
}),
]
: []),
],
initialScope: { tags: { app: opts.app } },
});