Initial commit
This commit is contained in:
50
apps/web-tanstack/src/instrumentation-client.test.ts
Normal file
50
apps/web-tanstack/src/instrumentation-client.test.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
// apps/web-tanstack/src/instrumentation-client.test.ts
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
// Hoist the mock so it's active when instrumentation-client runs its
|
||||
// top-level initSentryClientReact call on import.
|
||||
const initSentryClientReactMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("@repo/core-shared/instrumentation/sentry/init-client-react", () => ({
|
||||
initSentryClientReact: initSentryClientReactMock,
|
||||
}));
|
||||
|
||||
describe("instrumentation-client", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
it("passes nonce from csp-nonce meta tag to initSentryClientReact", async () => {
|
||||
const meta = document.createElement("meta");
|
||||
meta.setAttribute("name", "csp-nonce");
|
||||
meta.setAttribute("content", "test-nonce-xyz");
|
||||
document.head.appendChild(meta);
|
||||
|
||||
try {
|
||||
await import("./instrumentation-client");
|
||||
} finally {
|
||||
document.head.removeChild(meta);
|
||||
}
|
||||
|
||||
expect(initSentryClientReactMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ nonce: "test-nonce-xyz" }),
|
||||
);
|
||||
});
|
||||
|
||||
it("passes empty string nonce when no csp-nonce meta tag is present", async () => {
|
||||
await import("./instrumentation-client");
|
||||
|
||||
expect(initSentryClientReactMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ nonce: "" }),
|
||||
);
|
||||
});
|
||||
|
||||
it("passes web-tanstack as the app tag", async () => {
|
||||
await import("./instrumentation-client");
|
||||
|
||||
expect(initSentryClientReactMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ app: "web-tanstack" }),
|
||||
);
|
||||
});
|
||||
});
|
||||
18
apps/web-tanstack/src/instrumentation-client.ts
Normal file
18
apps/web-tanstack/src/instrumentation-client.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
// apps/web-tanstack/src/instrumentation-client.ts
|
||||
// Browser-entry hook. Imported at the top of the client entry file.
|
||||
import { initSentryClientReact } from "@repo/core-shared/instrumentation/sentry/init-client-react";
|
||||
|
||||
function getNonce(): string {
|
||||
if (typeof document === "undefined") return "";
|
||||
return (
|
||||
document.querySelector('meta[name="csp-nonce"]')?.getAttribute("content") ??
|
||||
""
|
||||
);
|
||||
}
|
||||
|
||||
initSentryClientReact({
|
||||
dsn: import.meta.env["VITE_WEB_TANSTACK_SENTRY_DSN"],
|
||||
app: "web-tanstack",
|
||||
release: import.meta.env["VITE_GIT_COMMIT_SHA"],
|
||||
nonce: getNonce(),
|
||||
});
|
||||
12
apps/web-tanstack/src/instrumentation.ts
Normal file
12
apps/web-tanstack/src/instrumentation.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
// apps/web-tanstack/src/instrumentation.ts
|
||||
// Server-entry hook. Imported at the top of the server entry file before any
|
||||
// request handler runs. Initializes the OTel SDK here so PII scrub processors
|
||||
// are active from the very first request (C1 fix — closes the startup window
|
||||
// where Sentry auto-instrumentation could send unscrubbed errors).
|
||||
import { initOtelServerNode } from "@repo/core-shared/instrumentation/otel/init-server-node";
|
||||
|
||||
initOtelServerNode({
|
||||
dsn: process.env["WEB_TANSTACK_SENTRY_DSN"] ?? "",
|
||||
serviceName: "web-tanstack",
|
||||
environment: process.env["NODE_ENV"] ?? "development",
|
||||
});
|
||||
29
apps/web-tanstack/src/routes/__root.test.tsx
Normal file
29
apps/web-tanstack/src/routes/__root.test.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import "@testing-library/jest-dom";
|
||||
|
||||
// Mock @tanstack/react-router so we don't need a full router context.
|
||||
// useLoaderData is supplied so the component can read the nonce from loader data.
|
||||
vi.mock("@tanstack/react-router", () => ({
|
||||
createRootRoute: vi.fn((opts: { component: React.ComponentType }) => ({
|
||||
options: { component: opts.component },
|
||||
useLoaderData: () => ({ nonce: "test-nonce-abc" }),
|
||||
})),
|
||||
Outlet: () => <div data-testid="outlet" />,
|
||||
}));
|
||||
|
||||
describe("Root route", () => {
|
||||
it("renders csp-nonce meta tag and Outlet", async () => {
|
||||
const { Route } = await import("./__root");
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const RootComponent = (Route as any).options
|
||||
.component as React.ComponentType;
|
||||
|
||||
render(<RootComponent />);
|
||||
|
||||
expect(screen.getByTestId("outlet")).toBeInTheDocument();
|
||||
const metaTag = document.querySelector('meta[name="csp-nonce"]');
|
||||
expect(metaTag).toBeInTheDocument();
|
||||
expect(metaTag?.getAttribute("content")).toBe("test-nonce-abc");
|
||||
});
|
||||
});
|
||||
25
apps/web-tanstack/src/routes/__root.tsx
Normal file
25
apps/web-tanstack/src/routes/__root.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Outlet, createRootRoute } from "@tanstack/react-router";
|
||||
import { getNonce } from "@repo/core-shared/security/tanstack";
|
||||
|
||||
export const Route = createRootRoute({
|
||||
loader: async () => {
|
||||
try {
|
||||
// Server-side during SSR: read nonce set by applySecurityHeaders middleware.
|
||||
// Fails gracefully on client-side navigation (nonce already in DOM from SSR).
|
||||
const { getEvent } = await import("vinxi/http");
|
||||
return { nonce: getNonce(getEvent().node.req) };
|
||||
} catch {
|
||||
return { nonce: "" };
|
||||
}
|
||||
},
|
||||
component: () => {
|
||||
const { nonce } = Route.useLoaderData();
|
||||
return (
|
||||
<>
|
||||
{/* nonce exposed to client so instrumentation-client.ts can read it */}
|
||||
<meta name="csp-nonce" content={nonce} />
|
||||
<Outlet />
|
||||
</>
|
||||
);
|
||||
},
|
||||
});
|
||||
14
apps/web-tanstack/src/routes/index.tsx
Normal file
14
apps/web-tanstack/src/routes/index.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const Route = createFileRoute("/" as any)({
|
||||
component: Home,
|
||||
});
|
||||
|
||||
function Home() {
|
||||
return (
|
||||
<main>
|
||||
<p>This page is rendered by TanStack Router and consumes the same feature packages as the Next.js app.</p>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
11
apps/web-tanstack/src/vite-env.d.ts
vendored
Normal file
11
apps/web-tanstack/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
// Minimal Vite-style env typing for the instrumentation-client entry.
|
||||
// When the full TanStack Start / Vite build is wired in a later plan,
|
||||
// replace this with `/// <reference types="vite/client" />`.
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_WEB_TANSTACK_SENTRY_DSN?: string;
|
||||
readonly VITE_GIT_COMMIT_SHA?: string;
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
||||
Reference in New Issue
Block a user