Skip to content

[feat] ENG-3684 - Carry the production marker to server-rendered builds - #165

Merged
mariojgt merged 3 commits into
mainfrom
mariot/eng-3684-production-marker-ssr-builds
Aug 24, 2026
Merged

mariojgt merged 3 commits into
mainfrom
mariot/eng-3684-production-marker-ssr-builds

Conversation

@mariojgt

Copy link
Copy Markdown
Contributor

Closes ENG-3684

What changed

The connector injects two things, by two different routes. The widget tag goes into source, so it ships with the app. __PATCHSTACK_PROD__ gets stamped into built HTML — and a server-rendered root never produces a static HTML file to stamp. On those stacks the marker never arrives, the widget reads the published site as build mode, and visitors get the claim flow instead of the report form.

mark-build hid it. Finding a build directory with no HTML in it printed Nothing to mark and exited 0, which looks exactly like success.

  • guide now asks for the marker whenever the root shell is code rather than HTML. React-family roots get a literal JSX snippet; the rest get the requirement described, because their head mechanism (useHead, <svelte:head>, Astro frontmatter) is not a plain script tag and a wrong snippet costs more than an accurate sentence.
  • It counts as an outstanding step, so the checklist stops reporting complete while the marker is missing.
  • The snippet is gated on each framework's own production expression — import.meta.env.PROD where the bundler defines it, process.env.NODE_ENV for Next/Gatsby, !import.meta.dev for Nuxt. The gate is load-bearing: an ungated marker also fires in the hosted builder's preview, which is where the owner still needs the claim flow.
  • The marker stays an inline document script rather than a module-level assignment. The widget tag is defer, so only a parser-executed inline script is ordered ahead of its init for certain.
  • mark-build now names the server-rendered case and points at guide.

Verification

npm test — 1307 passed, 6 skipped, 106 files. npm run typecheck clean.

Smoke-tested against a fixture mirroring the reported site (TanStack Start, widget tag already in __root.tsx), which reproduces its exact state:

 ✔ Disclosure widget installed
 ✖ Add the production marker yourself — this root is server-rendered, so mark-build cannot stamp it
     Without it the widget treats the published site as build mode and shows the claim flow to visitors.
     Edit src/routes/__root.tsx → put it in <head>, above the widget tag:
       {import.meta.env.PROD && (
         <script
           data-patchstack-build="true"
           dangerouslySetInnerHTML={{ __html: 'window.__PATCHSTACK_PROD__=true;' }}
         />
       )}
     The import.meta.env.PROD guard is required — an ungated marker also hides the claim flow in preview.

Before merging

This changes src/guide.ts checklist output, so per CLAUDE.md it needs node field-test/run.mjs --persona hostile --rounds 3. I have not run it — it needs a real agent and 30–50k tokens per round.

Worth a reviewer's opinion: this asks the developer to paste a snippet. The alternative is having the installed server middleware inject the marker into SSR responses, which is automatic but only covers deployments where protect is wired. I went with the snippet because it matches how the widget tag already gets placed on these stacks and has no runtime dependency.

🤖 Generated with Claude Code

The connector has two injection paths and only one of them reaches a
server-rendered deploy. The widget tag is written into source, so it ships
with the app; `__PATCHSTACK_PROD__` is stamped into built HTML, and a
server-rendered root never produces a static HTML file to stamp. On those
stacks the marker never arrives, so the widget reads the live site as build
mode and shows the claim flow to visitors.

`mark-build` made this invisible: finding a build directory with no HTML in
it printed "Nothing to mark" and exited 0, which is indistinguishable from
success.

- `guide` now asks for the marker whenever the root shell is code rather
  than HTML, printing the JSX snippet for React-family roots and describing
  the requirement for the rest, and counts it as an outstanding step.
- The snippet is gated on the framework's own production expression. An
  ungated marker would also fire in the hosted builder's preview, which is
  where the owner still needs the claim flow.
- The marker stays an inline document script. The widget tag is `defer`, so
  only a parser-executed script is ordered ahead of its init for certain.
- `mark-build` now names the server-rendered case and points at `guide`
  instead of reporting nothing to do.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderbuds

coderbuds Bot commented Aug 24, 2026 •

Copy link
Copy Markdown

Production marker integration is well-structured with thorough test coverage.

🎯 Quality: 100% Elite · 📦 Size: Medium

📈 This month: Your 62nd PR — above team average · Averaging Excellent

See how your team is trending →

@mariojgt

Copy link
Copy Markdown
Contributor Author

/review

mariojgt and others added 2 commits August 24, 2026 10:48
The marker only reaches production if it is in the source shell before the
build runs, and asking the developer to paste it there makes the one step
that must never be forgotten the one step nobody automates.

`scan` is already a pre-build hook, and it already edits the source shell to
place the widget tag on plain HTML projects. It now does the same for the
marker on JSX roots: insert it above the widget tag so document order puts it
first, fall back to `<head>` then `<body>`, and wrap it in the same managed
`#region` fence the protect installer uses so a re-run refreshes the block
rather than stacking copies. A marker the developer placed by hand is adopted,
not duplicated.

That makes the flag arrive on its own from `bun run build` or `npm run build`,
the same way the dependency scan and the widget tag already do.

`guide` now points at `scan` for this and keeps the snippet as the fallback
for roots where no anchor was found.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Build scripts commonly chain the scan as `patchstack-connect scan || true`, so
a failed or offline post is expected to be survivable. Running the marker pass
after `postManifest` made it the opposite: any network error skipped the write
and the build shipped without a production flag, silently.

The marker carries no site UUID and needs no round-trip, so nothing about it
justified waiting on the server. It now runs as soon as the framework is known
and before the post, still after the --dry-run return so a preview stays
side-effect free.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@mariojgt
mariojgt merged commit 8a7b8d5 into main Aug 24, 2026
6 checks passed
@mariojgt
mariojgt deleted the mariot/eng-3684-production-marker-ssr-builds branch August 24, 2026 09:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants