feat(eslint+ci): R40 boundary rule for @sentry/* + R31 sendDefaultPii grep gate
Adds two flat-config blocks to core-eslint/base.js: (1) repo-wide
no-restricted-imports for @sentry/* with the R40 message, (2) an
allowlist override for the only paths permitted to import the Sentry
SDK directly — core-shared/instrumentation/sentry/**, the bind-sentry
DI files, the no-sentry test guards, and apps' instrumentation* /
next.config / vite.config / sentry.*.config files. Patterns use
**/-prefix so they match whether ESLint runs from the repo root or
from inside a sub-package.
Also adds the standard `argsIgnorePattern: "^_"` config (used
throughout the repo) and a Node-globals override for *.mjs/*.cjs/*.js
and *.config.{ts,tsx} so withSentryConfig in next.config.mjs lints
clean. Required adding `globals` as a core-eslint dep.
Adds .github/workflows/sentry-pii-guard.yml — a lightweight CI step
that fails any PR introducing `sendDefaultPii: true` (R31). Excludes
node_modules / dist / .next / .turbo from the grep so vendored SDK
JSDoc examples don't false-positive.
Pre-existing lint nits cleared as part of getting `pnpm lint` green:
- core-testing define-contract-suite.test.ts: void the unused
receivedTracer (mirrors the next test's pattern)
- marketing-pages bind-dev-seed.ts: drop unused MockSiteSettingsRepository
import
- marketing-pages get-site-settings.use-case.ts: drop the now-redundant
eslint-disable for `_input`
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
28
.github/workflows/sentry-pii-guard.yml
vendored
Normal file
28
.github/workflows/sentry-pii-guard.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# R31 — block sendDefaultPii: true from ever landing.
|
||||||
|
#
|
||||||
|
# This is a defense-in-depth gate: the privacy posture is also enforced by
|
||||||
|
# the centralized init helpers in core-shared/instrumentation/sentry/, but
|
||||||
|
# this grep makes any drift impossible to merge.
|
||||||
|
|
||||||
|
name: Sentry PII guard (R31)
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
pii-guard:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Verify sendDefaultPii is never true
|
||||||
|
run: |
|
||||||
|
if grep -RIn --include='*.ts' --include='*.tsx' --include='*.mjs' --include='*.cjs' --include='*.js' \
|
||||||
|
--exclude-dir=node_modules --exclude-dir=.next --exclude-dir=dist --exclude-dir=.turbo \
|
||||||
|
-E 'sendDefaultPii\s*:\s*true' \
|
||||||
|
packages/ apps/; then
|
||||||
|
echo "::error::R31 violation — sendDefaultPii: true is forbidden anywhere in the repo."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "OK — no sendDefaultPii: true detected."
|
||||||
@@ -3,10 +3,17 @@ import eslintConfigPrettier from "eslint-config-prettier";
|
|||||||
import tseslint from "typescript-eslint";
|
import tseslint from "typescript-eslint";
|
||||||
import turboPlugin from "eslint-plugin-turbo";
|
import turboPlugin from "eslint-plugin-turbo";
|
||||||
import boundaries from "eslint-plugin-boundaries";
|
import boundaries from "eslint-plugin-boundaries";
|
||||||
|
import globals from "globals";
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
{ ignores: ["dist/**", "node_modules/**", ".next/**", ".turbo/**", "storybook-static/**"] },
|
{ ignores: ["dist/**", "node_modules/**", ".next/**", ".turbo/**", "storybook-static/**"] },
|
||||||
js.configs.recommended,
|
js.configs.recommended,
|
||||||
|
{
|
||||||
|
files: ["**/*.{mjs,cjs,js}", "**/*.config.{ts,tsx}"],
|
||||||
|
languageOptions: {
|
||||||
|
globals: { ...globals.node },
|
||||||
|
},
|
||||||
|
},
|
||||||
...tseslint.configs.recommended,
|
...tseslint.configs.recommended,
|
||||||
eslintConfigPrettier,
|
eslintConfigPrettier,
|
||||||
{
|
{
|
||||||
@@ -15,6 +22,19 @@ export default [
|
|||||||
"turbo/no-undeclared-env-vars": "warn",
|
"turbo/no-undeclared-env-vars": "warn",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
// Honour the leading-underscore convention for intentionally-unused params/vars.
|
||||||
|
"@typescript-eslint/no-unused-vars": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
argsIgnorePattern: "^_",
|
||||||
|
varsIgnorePattern: "^_",
|
||||||
|
caughtErrorsIgnorePattern: "^_",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
plugins: { boundaries },
|
plugins: { boundaries },
|
||||||
settings: {
|
settings: {
|
||||||
@@ -45,4 +65,42 @@ export default [
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// R40 — block direct @sentry/* imports outside the allowlisted instrumentation paths
|
||||||
|
{
|
||||||
|
files: ["**/*.{ts,tsx,mjs,cjs,js}"],
|
||||||
|
rules: {
|
||||||
|
"no-restricted-imports": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
patterns: [
|
||||||
|
{
|
||||||
|
group: ["@sentry/*"],
|
||||||
|
message:
|
||||||
|
"Import from @repo/core-shared/instrumentation instead — feature packages must not depend on Sentry directly (R40).",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// R40 allowlist — the only paths permitted to import @sentry/*.
|
||||||
|
// Patterns are double-star prefixed so they match whether eslint runs from
|
||||||
|
// the repo root or from inside a sub-package.
|
||||||
|
{
|
||||||
|
files: [
|
||||||
|
"**/instrumentation/sentry/**",
|
||||||
|
"**/instrumentation/di/bind-sentry-instrumentation.{ts,js}",
|
||||||
|
"**/instrumentation/di/bind-sentry-instrumentation.test.{ts,js}",
|
||||||
|
"**/setup/no-sentry.{ts,js}",
|
||||||
|
"**/setup/no-sentry.test.{ts,js}",
|
||||||
|
"**/instrumentation.{ts,js,mjs}",
|
||||||
|
"**/instrumentation-client.{ts,js,mjs}",
|
||||||
|
"**/next.config.{mjs,ts,js}",
|
||||||
|
"**/vite.config.{ts,mjs,js}",
|
||||||
|
"**/sentry.*.config.{ts,mjs,js}",
|
||||||
|
],
|
||||||
|
rules: {
|
||||||
|
"no-restricted-imports": "off",
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -17,5 +17,8 @@
|
|||||||
"eslint-plugin-boundaries": "^4.2.2",
|
"eslint-plugin-boundaries": "^4.2.2",
|
||||||
"eslint-plugin-turbo": "^2.4.0",
|
"eslint-plugin-turbo": "^2.4.0",
|
||||||
"typescript-eslint": "^8.25.0"
|
"typescript-eslint": "^8.25.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"globals": "^17.6.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ describe("defineContractSuite — getTracer plumbing (R50)", () => {
|
|||||||
// Vitest defers actual assertion to the `it`; we verify the wiring by re-reading after.
|
// Vitest defers actual assertion to the `it`; we verify the wiring by re-reading after.
|
||||||
// (This is a meta-test of plumbing only — the inner it() runs as a child describe.)
|
// (This is a meta-test of plumbing only — the inner it() runs as a child describe.)
|
||||||
expect(typeof tracer.startSpan).toBe("function");
|
expect(typeof tracer.startSpan).toBe("function");
|
||||||
|
void receivedTracer;
|
||||||
});
|
});
|
||||||
|
|
||||||
it("getTracer is undefined when opts.tracer not provided (backward compat)", () => {
|
it("getTracer is undefined when opts.tracer not provided (backward compat)", () => {
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ export type IGetSiteSettingsUseCase = ReturnType<typeof getSiteSettingsUseCase>;
|
|||||||
|
|
||||||
export const getSiteSettingsUseCase =
|
export const getSiteSettingsUseCase =
|
||||||
(siteSettingsRepository: ISiteSettingsRepository) =>
|
(siteSettingsRepository: ISiteSettingsRepository) =>
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
async (_input: GetSiteSettingsInput): Promise<GetSiteSettingsOutput> => {
|
async (_input: GetSiteSettingsInput): Promise<GetSiteSettingsOutput> => {
|
||||||
const result = await siteSettingsRepository.getSiteSettings();
|
const result = await siteSettingsRepository.getSiteSettings();
|
||||||
return getSiteSettingsOutputSchema.parse(result);
|
return getSiteSettingsOutputSchema.parse(result);
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
import { marketingPagesContainer } from "./container.js";
|
import { marketingPagesContainer } from "./container.js";
|
||||||
import { MARKETING_PAGES_SYMBOLS } from "./symbols.js";
|
import { MARKETING_PAGES_SYMBOLS } from "./symbols.js";
|
||||||
import { MockPagesRepository } from "../infrastructure/repositories/pages.repository.mock.js";
|
import { MockPagesRepository } from "../infrastructure/repositories/pages.repository.mock.js";
|
||||||
import { MockSiteSettingsRepository } from "../infrastructure/repositories/site-settings.repository.mock.js";
|
|
||||||
import { buildDevPages, buildDevSiteSettings } from "../__seeds__/dev.js";
|
import { buildDevPages, buildDevSiteSettings } from "../__seeds__/dev.js";
|
||||||
import { getSiteSettingsUseCase } from "../application/use-cases/get-site-settings.use-case.js";
|
import { getSiteSettingsUseCase } from "../application/use-cases/get-site-settings.use-case.js";
|
||||||
import { getPageBySlugUseCase } from "../application/use-cases/get-page-by-slug.use-case.js";
|
import { getPageBySlugUseCase } from "../application/use-cases/get-page-by-slug.use-case.js";
|
||||||
|
|||||||
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
@@ -480,6 +480,10 @@ importers:
|
|||||||
version: 3.2.4(@types/debug@4.1.13)(@types/node@22.19.17)(happy-dom@20.8.9)(jiti@2.6.1)(jsdom@25.0.1)(lightningcss@1.32.0)(sass@1.99.0)(terser@5.46.2)(tsx@4.21.0)
|
version: 3.2.4(@types/debug@4.1.13)(@types/node@22.19.17)(happy-dom@20.8.9)(jiti@2.6.1)(jsdom@25.0.1)(lightningcss@1.32.0)(sass@1.99.0)(terser@5.46.2)(tsx@4.21.0)
|
||||||
|
|
||||||
packages/core-eslint:
|
packages/core-eslint:
|
||||||
|
dependencies:
|
||||||
|
globals:
|
||||||
|
specifier: ^17.6.0
|
||||||
|
version: 17.6.0
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@eslint/js':
|
'@eslint/js':
|
||||||
specifier: ^9.20.0
|
specifier: ^9.20.0
|
||||||
@@ -4956,6 +4960,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
|
resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
globals@17.6.0:
|
||||||
|
resolution: {integrity: sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
gopd@1.2.0:
|
gopd@1.2.0:
|
||||||
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
|
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -11864,6 +11872,8 @@ snapshots:
|
|||||||
|
|
||||||
globals@14.0.0: {}
|
globals@14.0.0: {}
|
||||||
|
|
||||||
|
globals@17.6.0: {}
|
||||||
|
|
||||||
gopd@1.2.0: {}
|
gopd@1.2.0: {}
|
||||||
|
|
||||||
graceful-fs@4.2.11: {}
|
graceful-fs@4.2.11: {}
|
||||||
|
|||||||
Reference in New Issue
Block a user