Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/hydrogen-wrapper-prompt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@subtextdev/subtext-wizard': patch
---

Teach the install prompt the Shopify Hydrogen route: detect `@shopify/hydrogen` (before its Remix/React Router/Vite dependencies can shadow the match) and install via the `@subtextdev/hydrogen` wrapper package — snippet + CSP nonce via `<Subtext>`, `withSubtextCSP` in `entry.server`, commerce events + consent-gated capture via `<SubtextAnalytics>`, identity via `useSubtextIdentity`, and analytics linkage via `onSessionUrl` — instead of pasting the raw inline snippet. Includes the optional order → session linkage (`attachSessionToCart`: session URL as a `_`-prefixed cart attribute that lands on the order in the Shopify admin, with the `mode: 'ref'` fallback when notification templates can't be vouched for). `@subtextdev/hydrogen` also counts as "already installed" in the pre-check.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
<img width="2560" height="658" alt="LOCKUP - HORZ - BY FS - LIGHT - COLOR (3)" src="https://github.com/user-attachments/assets/3f0da600-52c8-4fa1-a203-1918e6a6cfda" />


![NPM Downloads](https://img.shields.io/npm/dm/%40subtextdev%2Fsubtext-wizard?style=flat-square&labelColor=240046&color=7b2cbf)
![NPM Last Updated](https://img.shields.io/npm/last-update/%40subtextdev%2Fsubtext-wizard?style=flat-square&labelColor=3c096c&color=9d4edd)
![NPM Version](https://img.shields.io/npm/v/%40subtextdev%2Fsubtext-wizard?style=flat-square&labelColor=5a189a&color=e0aaff)
![NPM Downloads](https://img.shields.io/npm/dm/%40subtextdev%2Fsubtext-wizard?style=flat-square&labelColor=46001f&color=b81b56)
![NPM Last Updated](https://img.shields.io/npm/last-update/%40subtextdev%2Fsubtext-wizard?style=flat-square&labelColor=6b0f36&color=f5447b)
![NPM Version](https://img.shields.io/npm/v/%40subtextdev%2Fsubtext-wizard?style=flat-square&labelColor=9a1847&color=ffd6e4)

**Session replay, built for agents.** Subtext is agentic session review: it captures production sessions of your app and connects them to your coding agent — Claude Code, Cursor, Codex, Devin, your own harness — so it can review what real users did, reproduce reported bugs, verify its own UI changes, and manage capture privacy rules, all without leaving the terminal.

Expand Down
13 changes: 12 additions & 1 deletion templates/install-prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Determine whether the capture snippet is *actually* installed. The snippet is th

Run these checks in order. Stop at the first positive match.

1. Package dependencies (highest confidence) — Read `package.json`. Look for `@fullstory/browser`, `@fullstory/react-native`, or `@fullstory/snippet` in `dependencies` or `devDependencies`.
1. Package dependencies (highest confidence) — Read `package.json`. Look for `@fullstory/browser`, `@fullstory/react-native`, `@fullstory/snippet`, or `@subtextdev/hydrogen` in `dependencies` or `devDependencies`.
2. Script tag in HTML entry point — Search ONLY the HTML entry point (`index.html`, `app/layout.tsx`, `pages/_document.tsx`, or framework equivalent) for the literal strings `fullstory.com/s/fs.js` or `_fs_script`. Do not search other files.
3. SDK initialization call — Grep for `init\(\s*\{\s*orgId`, `window\['_fs_org'\]\s*=`, or `window\._fs_org\s*=` in `.ts`, `.tsx`, `.js`, `.jsx` files (exclude `node_modules`, test files, and `*.d.ts`). The `init()` call comes from `@fullstory/browser` v2: `import { init } from '@fullstory/browser'`.

Expand All @@ -45,6 +45,7 @@ Before making any changes, do a read-only pass to gather what the install will d

Read `package.json` and project structure to detect the framework:

- `@shopify/hydrogen` in dependencies → Shopify Hydrogen → wrapper-package install, see "Shopify Hydrogen" under Framework patterns. Check this FIRST: Hydrogen apps also contain Remix/React Router and Vite dependencies, which must not shadow this match.
- `next` in dependencies → Next.js (App Router) → `app/layout.tsx`
- `next` + `pages/_document.tsx` exists → Next.js (Pages Router) → `pages/_document.tsx`
- `@remix-run/*` in dependencies → Remix → `app/root.tsx`
Expand Down Expand Up @@ -99,6 +100,16 @@ This is the snippet to install. It is specific to this organization — install

### Framework patterns

- Shopify Hydrogen — Do NOT paste the raw snippet. Install the wrapper package instead; it handles the snippet, Hydrogen's nonce-based CSP, and consent gating through Shopify's Customer Privacy API:
1. `npm install @subtextdev/hydrogen` (match the project's package manager: pnpm/yarn/bun if a corresponding lockfile exists).
2. Derive the configuration from the snippet in Step 4 above — do not invent values: `orgId` is the `window['_fs_org']` value; if `window['_fs_host']` is `eu1.fullstory.com` pass `region="eu"`; if the host values are anything other than `fullstory.com`/`eu1.fullstory.com` standards, pass them through as `host`/`script` props.
3. `app/root.tsx` — add `<Subtext orgId="…" />` (plus `region`/`host`/`script` if derived above) inside `<head>` of the root layout. The component attaches Hydrogen's CSP nonce automatically via `useNonce()`.
4. `app/entry.server.tsx` — wrap the existing directives object: `createContentSecurityPolicy(withSubtextCSP({...existing directives}))`, importing `withSubtextCSP` from `@subtextdev/hydrogen` (pass `{region: 'eu'}` as its second argument for EU orgs). This replaces the manual CSP directive edits from Step 2 — do not also add the directives by hand.
5. `app/root.tsx` — add `<SubtextAnalytics />` anywhere inside `<Analytics.Provider>`. It forwards Hydrogen's commerce events (`product_viewed`, `cart_updated`, search, collection, add/remove-to-cart) into capture and starts/stops capture as Customer Privacy consent changes.
6. For Step 5 (identity), use the package's `useSubtextIdentity(customer && {uid: customer.id, displayName, email})` hook where the authenticated customer is available on the client (typically the account route), instead of a raw `FS('setIdentity')` call.
7. For Step 6 (analytics linkage), pass `onSessionUrl` to `<SubtextAnalytics>` instead of calling `FS('getSession')` directly — it fires with the session URL every time capture starts, which with consent gating can be well after page load: `<SubtextAnalytics onSessionUrl={(url) => { /* attach subtext_url to each detected tool here */ }} />`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hydrogen overrides lost in later steps

Medium Severity

Hydrogen-specific identity and analytics guidance appears only as a brief note under Step 4, while Steps 5 and 6 still require raw FS('setIdentity') and FS('getSession') with no Hydrogen exception. Agents following those later steps can skip useSubtextIdentity and onSessionUrl, so consent-gated session URL linkage silently gets nothing.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit ea63429. Configure here.

8. Order → session linkage (optional but recommended — ask the user, or include it in autonomous mode): pass `attachSessionToCart` to `<SubtextAnalytics>`. It writes the session URL onto the cart as a `_subtext_url` custom attribute, which Shopify carries through checkout onto the order — visible in the Shopify admin under "Additional details" on every order. This requires the app's cart route action to handle `CartForm.ACTIONS.AttributesUpdateInput`; if its switch lacks that case, add: `case CartForm.ACTIONS.AttributesUpdateInput: result = await cart.updateAttributes(inputs.attributes); break;`. If the cart route is not at `/cart`, pass `attachSessionToCart={{cartRoute: '…'}}`. Leakage note to relay to the user: order attributes can appear in customized order-notification email templates (default Shopify templates do not render them; the `_` prefix is the convention templates use to filter internal attributes). The URL is auth-gated either way. If the user cannot vouch for their notification templates, use `attachSessionToCart={{mode: 'ref'}}`, which writes an opaque session id (`_subtext_ref`) instead of a clickable URL.
9. Capture is consent-gated by default, matching how Shopify gates its own analytics. Tell the user: Shopify's privacy banner does not load on default `*.oxygen` preview URLs, so capture will not start there — verify on a real domain. Also note checkout hands off to Shopify-hosted pages, which this integration does not capture.
- Next.js (App Router) — Add to `app/layout.tsx` inside `<head>`, or as a `<Script>` component with `dangerouslySetInnerHTML`.
- Next.js (Pages Router) — Add to `pages/_document.tsx` inside `<Head>`.
- Remix — Add to `app/root.tsx` inside `<head>` of the root layout.
Expand Down