feat: add observability-and-env skill with pino logger, Zod env schemas, and centralized Sentry bootstrap#6
feat: add observability-and-env skill with pino logger, Zod env schemas, and centralized Sentry bootstrap#6carlosvin wants to merge 11 commits into
Conversation
…as, and centralized Sentry bootstrap - Add src/env/runtimeEnvSchema.ts with shared DeploymentEnv and LogLevel Zod schemas - Add src/env/webEnv.ts parsing process.env once; derive browser-safe WebPublicEnvSchema slice - Add src/utils/logger.ts with createModuleLogger pino factory (no process.env inside) - Add src/utils/serverLogger.ts with createServerLogger bound to webServerEnv - Add instrument.env.mjs with resolveSentryBootstrapEnv() for plain-JS bootstrap - Add instrument.shared.mjs with initSentry() receiving pre-resolved values (no process.env inside) - Simplify instrument.server.mjs to 9 lines using the two shared helpers - Add src/middleware/webEnv.ts injecting ctx.context.publicEnv for server functions - Update observability/index.ts to use webPublicEnv.SENTRY_DSN instead of process.env - Update middleware/auth.ts to use webServerEnv.AUTH_HEADER_NAME instead of process.env - Update build script to copy all instrument.*.mjs files to .output/server - Update AGENTS.md §9 and §13 to document implemented patterns; remove window.__ENV__ guidance - Update .env.example with ENV, LOG_LEVEL, and canonical SENTRY_DSN - Add pino and pino-pretty dependencies - Register observability-and-env skill in skills/registry.json; regenerate skill artifacts Co-authored-by: Cursor <cursoragent@cursor.com>
…packages - Set allowBuilds for '@parcel/watcher', '@sentry/cli', '@swc/core', esbuild, protobufjs, and sharp to true for improved build configuration.
- Updated SKILL.md to include a new companion skill `observability-and-env`, detailing its purpose and usage. - Revised README.md to clarify the roles of both `tanstack-promptable-fullstack-app-template` and `observability-and-env` skills, emphasizing their architectural and operational focuses. - Added installation instructions for the observability companion skill to improve user guidance. - Updated portable documentation files to reflect the new skill structure and capabilities. This update aims to provide clearer guidance on using the TanStack skills and their respective functionalities.
Remove the checked-in absolute pnpm store path so Linux CI runners use their own writable default store instead of trying to mkdir /Users. Co-authored-by: Cursor <cursoragent@cursor.com>
Move the skills workflow to Node 24-capable action versions so GitHub no longer warns that the job relies on deprecated Node 20 JavaScript actions. Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Pull request overview
Introduces a centralized observability + environment-validation foundation for the TanStack Start template: Zod-validated env schemas split into server/public slices, a pino-based module logger factory with a server-bound convenience wrapper, and a three-file Sentry bootstrap pattern (instrument.env.mjs → instrument.shared.mjs → instrument.server.mjs). Existing process.env reads in auth.ts and observability/index.ts are replaced by reads from the validated singletons, and a new webEnvMiddleware plus a new observability-and-env skill are added to document the pattern.
Changes:
- New env schemas (
src/env/runtimeEnvSchema.ts,src/env/webEnv.ts) and pino logger factories (src/utils/logger.ts,src/utils/serverLogger.ts). - Refactored Sentry bootstrap into
instrument.env.mjs+instrument.shared.mjs, withinstrument.server.mjsreduced to a thin entry point; build script copies allinstrument.*.mjs. - New
observability-and-envskill (yaml + dist + generated SKILL.md) plus AGENTS.md/§9/§13 rewrite, README, registry, and pnpm-workspaceallowBuildsadditions;pino/pino-prettyadded as runtime deps.
Reviewed changes
Copilot reviewed 21 out of 24 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
src/env/runtimeEnvSchema.ts |
Shared DeploymentEnv/LogLevel enums and trim-to-undefined preprocessor. |
src/env/webEnv.ts |
WebServerEnvSchema + WebPublicEnvSchema singletons parsed from process.env. |
src/utils/logger.ts |
Lazy-singleton root pino logger + createModuleLogger factory. |
src/utils/serverLogger.ts |
createServerLogger bound to webServerEnv env values. |
src/middleware/webEnv.ts |
New middleware injecting publicEnv into server-fn context (not yet registered). |
src/middleware/auth.ts |
Reads AUTH_HEADER_NAME from webServerEnv instead of process.env. |
src/services/observability/index.ts |
DSN check now uses webPublicEnv.SENTRY_DSN. |
instrument.env.mjs |
New plain-JS env resolver for Sentry bootstrap. |
instrument.shared.mjs |
New initSentry helper; pre-resolved values; no env reads. |
instrument.server.mjs |
Reduced to resolve+init using the new helpers. |
package.json |
Adds pino, pino-pretty; build script copies instrument.*.mjs. |
pnpm-workspace.yaml |
New allowBuilds allow-list for native build scripts. |
pnpm-lock.yaml |
Lockfile churn for pino chain and removed transitive rollup peer pinning. |
.env.example |
Documents ENV, LOG_LEVEL, SENTRY_DSN (and legacy VITE_SENTRY_DSN). |
AGENTS.md |
§9 documents pino + Sentry bootstrap; §13 replaces window.__ENV__ with loader/middleware pattern. |
skills/src/observability-and-env.skill.yaml |
New skill source. |
skills/src/tanstack-promptable-fullstack-app-template.skill.yaml |
Cross-link to companion skill. |
skills/registry.json |
Registers the new skill. |
skills/README.md |
Documents both skills + install command. |
skills/dist/observability-and-env.md |
Generated portable copy. |
skills/dist/tanstack-promptable-fullstack-app-template.md |
Cross-link diff. |
.agents/skills/observability-and-env/SKILL.md |
Generated agent-facing skill artifact. |
.agents/skills/tanstack-promptable-fullstack-app-template/SKILL.md |
Cross-link diff. |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
Comments suppressed due to low confidence (2)
src/env/webEnv.ts:49
- The precedence here resolves
VITE_SENTRY_DSN || SENTRY_DSN, meaning the legacy alias wins over the canonical variable. The comment on line 25 callsSENTRY_DSNthe canonical name andVITE_SENTRY_DSNthe backwards-compatible alias, so during a migration where both are set the documented "use SENTRY_DSN in new config" guidance is contradicted at runtime. ConsiderwebServerEnv.SENTRY_DSN || webServerEnv.VITE_SENTRY_DSNso the canonical value takes priority.
export const webPublicEnv: WebPublicEnv = WebPublicEnvSchema.parse({
...webServerEnv,
SENTRY_DSN: webServerEnv.VITE_SENTRY_DSN || webServerEnv.SENTRY_DSN,
})
src/utils/logger.ts:52
src/utils/logger.tsis described as safe to import from client React components, and pino + pino-pretty are listed as runtimedependenciesinpackage.json. Bundling pino into the browser bundle is heavy (pretty transport spawns a worker thread, etc.) and largely useless on the client. Even with theprocess.stdoutguard, all the pino code (and pino-pretty's worker target string) ends up in the client bundle. Consider either keeping the logger server-only (and providing a thin browser shim) or moving these todevDependencies/optionalDependencieswith a dynamic import on the server path.
// `createModuleLogger` is also imported by client React components, so this
// must stay safe in a browser bundle where `process.stdout` is undefined.
// Pretty transport is only enabled in an interactive Node TTY outside of
// production.
const isNodeTty = typeof process !== 'undefined' && process.stdout != null && Boolean(process.stdout.isTTY)
const useTtyPretty = isNodeTty && environment !== 'production'
// Root is created at the most permissive level ('trace') so per-child level
// overrides are never filtered out at the root.
rootLogger = useTtyPretty
? pino(
{ level: 'trace' },
pino.transport({
target: 'pino-pretty',
options: { colorize: true, singleLine: true, translateTime: 'HH:MM:ss.l' },
}),
)
: pino({ level: 'trace' })
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…tion - Replaced all instances of `VITE_SENTRY_DSN` with `SENTRY_DSN` across the codebase for consistency. - Updated environment validation in `instrument.env.mjs` to utilize a shared schema from `instrument.env.shared.mjs`, ensuring strict validation of `NODE_ENV` and `SENTRY_DSN`. - Enhanced documentation in AGENTS.md, README.md, and various skill files to reflect the changes in environment variable handling and Sentry configuration. - Added tests for environment resolution to ensure correct behavior under various configurations. This refactor aims to streamline observability setup and improve clarity in environment management.
- Expose validated public env via getWebPublicEnv server fn and root loader - Wire client observability from loader data in AppLayout (no webEnv in browser) - Use OptionalTrimmedStringSchema for AUTH_HEADER_NAME - Warn when SENTRY_DSN is missing in non-development bootstrap - Align docs/skills on a short instrument.server entry (drop line-count drift) Co-authored-by: Cursor <cursoragent@cursor.com>
- Transitioned to using a GET server function for fetching public environment variables (ENV, LOG_LEVEL, SENTRY_DSN) in React components, eliminating direct imports of `webEnv` in client bundles. - Adjusted AppLayout to remove publicEnv prop and related observability logic, streamlining component structure. - Updated documentation and skills to reflect the new approach for accessing public environment variables via route loaders. This refactor enhances the separation of server and client concerns, ensuring a cleaner architecture.
- Replaced plain ESM files with TypeScript modules for observability setup, including `instrument.env.mts`, `instrument.shared.mts`, and `instrument.server.mts`. - Updated the build process to compile TypeScript and emit `.mjs` files for production. - Enhanced environment validation and Sentry initialization logic, ensuring strict adherence to schema validation. - Refactored middleware to streamline public environment handling, chaining `authMiddleware` with `webEnvMiddleware`. - Updated documentation and skills to reflect changes in file structure and usage patterns. This refactor aims to improve type safety, maintainability, and clarity in the observability setup.
… usage - Updated .env.example to include notes on the relationship between NODE_ENV and ENV for consistent deployment identity in Sentry. - Revised AGENTS.md to clarify the distinction between NODE_ENV and ENV, emphasizing the importance of keeping them aligned to avoid mislabeling Sentry events. - Adjusted various skill documentation to reflect changes in environment handling and logger configuration. - Improved comments in logger.ts and related files to ensure clarity on environment validation and usage. This update aims to improve understanding of environment configurations and their impact on observability.
What has been changed
This PR ports the observability and environment patterns proven in production to the template as a reusable skill + working code.
New files
src/env/runtimeEnvSchema.tsDeploymentEnv,LogLevelZod enums and optional preprocessorssrc/env/webEnv.tsWebServerEnvSchema(full) +WebPublicEnvSchema(browser-safe slice); singletons parsed oncesrc/utils/logger.tscreateModuleLogger(name, options)pino factory — noprocess.envinsidesrc/utils/serverLogger.tscreateServerLogger(name)— bindswebServerEnv.ENVandLOG_LEVELsrc/middleware/webEnv.tswebEnvMiddleware— injectsctx.context.publicEnvfor server functionsinstrument.env.mjsresolveSentryBootstrapEnv()— readsprocess.envonce before TS loadsinstrument.shared.mjsinitSentry({ serverName, dsn, environment, release })— all values pre-resolved by callerskills/src/observability-and-env.skill.yaml.agents/skills/observability-and-env/SKILL.mdModified files
instrument.server.mjsresolveSentryBootstrapEnv()+initSentry()src/services/observability/index.tswebPublicEnv.SENTRY_DSNinstead ofprocess.env.VITE_SENTRY_DSNsrc/middleware/auth.tswebServerEnv.AUTH_HEADER_NAMEinstead ofprocess.env.AUTH_HEADER_NAMEpackage.jsonpino,pino-pretty; copy allinstrument.*.mjsin build script.env.exampleENV,LOG_LEVEL, and canonicalSENTRY_DSNAGENTS.mdwindow.__ENV__with loader/middleware patternskills/registry.jsonKey invariants enforced
process.envis read only insrc/env/*.ts(schema parse) andinstrument.env.mjs(bootstrap)logLevel,environment) are passed as arguments — never resolved inside the factoryENV,LOG_LEVEL,SENTRY_DSN) reaches the browser via the root loader — nowindow.__ENV__globalchild()instancesTesting
npx tsc --noEmit— passes with no errorsnode ./scripts/skills/buildSkills.mjs --check— both skills validate cleanlynpx @biomejs/biome check src/env/ src/utils/ src/middleware/ src/services/observability/index.ts— no issuesMade with Cursor