@mhosaic/feedback-cli 0.13.0 → 0.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +109 -0
- package/dist/bin.js +15 -5
- package/dist/bin.js.map +1 -1
- package/dist/{init-64QSAHMB.js → init-P45CEXDP.js} +12 -1
- package/dist/init-P45CEXDP.js.map +1 -0
- package/dist/install-skill-3OPVFMTK.js +89 -0
- package/dist/install-skill-3OPVFMTK.js.map +1 -0
- package/dist/verify-LCXL3WOX.js +188 -0
- package/dist/verify-LCXL3WOX.js.map +1 -0
- package/package.json +4 -2
- package/skills/integrate-feedback/SKILL.md +163 -0
- package/skills/integrate-feedback/references/consumer-install-astro.md +83 -0
- package/skills/integrate-feedback/references/consumer-install-next.md +133 -0
- package/skills/integrate-feedback/references/consumer-install-nuxt.md +108 -0
- package/skills/integrate-feedback/references/consumer-install-plain.md +92 -0
- package/skills/integrate-feedback/references/consumer-install-remix.md +93 -0
- package/skills/integrate-feedback/references/consumer-install-svelte.md +87 -0
- package/skills/integrate-feedback/references/consumer-install-vite.md +100 -0
- package/skills/integrate-feedback/references/consumer-install-vue.md +92 -0
- package/skills/integrate-feedback/references/consumer-install.md +185 -0
- package/skills/integrate-feedback/references/identify-snippets.md +218 -0
- package/skills/integrate-feedback/references/operator-provision.md +235 -0
- package/skills/integrate-feedback/references/verify-install.md +151 -0
- package/dist/init-64QSAHMB.js.map +0 -1
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: integrate-feedback
|
|
3
|
+
description: Walk a teammate through integrating @mhosaic/feedback — either provisioning a project as the platform operator, or installing the widget in a host app as the consumer. Use when the user mentions adding the Mhosaic feedback widget, onboarding a new app, getting a public key, or wiring the widget into their codebase.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# /integrate-feedback — guided widget integration
|
|
7
|
+
|
|
8
|
+
You are guiding a teammate through integrating the **@mhosaic/feedback** widget into a project. The workflow splits into two modes:
|
|
9
|
+
|
|
10
|
+
- **Operator mode** — the platform admin (Victor or a delegate). Provisions a new Company / Project / public key on the Mhosaic backend. Has admin access to the software-factory admin SPA.
|
|
11
|
+
- **Consumer mode** — a teammate. Installs the widget in their own app, given a key + endpoint from the operator.
|
|
12
|
+
|
|
13
|
+
**Both modes are guided. Never assume — confirm at every checkpoint.**
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Step 0: Identify the mode
|
|
18
|
+
|
|
19
|
+
Ask the user which mode they're in. Use `AskUserQuestion` with:
|
|
20
|
+
|
|
21
|
+
- question: `Which side of the integration are you working on?`
|
|
22
|
+
- header: `Mode`
|
|
23
|
+
- options:
|
|
24
|
+
- label: `Provision (operator)`, description: `Create a new Company/Project/key in the Mhosaic backend. Requires admin SPA access.`
|
|
25
|
+
- label: `Install (consumer)`, description: `Add the widget to a host app. You already have a pk_proj_… key + endpoint from the operator.`
|
|
26
|
+
- label: `Both — start with operator, then consumer`, description: `Provision first, then keep going.`
|
|
27
|
+
|
|
28
|
+
Branch on the answer. Each branch is documented in detail under `references/`.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Step 0.5: Pre-flight (operator mode only)
|
|
33
|
+
|
|
34
|
+
If the user picked operator (or "Both"), confirm Chrome MCP is configured before going further. Operator mode hard-depends on `mcp__claude-in-chrome__*` tools — without them, the API-driving step at the heart of `references/operator-provision.md` can't run.
|
|
35
|
+
|
|
36
|
+
Check: call `mcp__claude-in-chrome__tabs_context_mcp` once. Possible outcomes:
|
|
37
|
+
|
|
38
|
+
- **Tool returns a tab list (even an empty one)** — Chrome MCP is installed. Proceed.
|
|
39
|
+
- **Tool isn't available / errors with "tool not found"** — Chrome MCP isn't configured on this Claude Code install. Tell the user:
|
|
40
|
+
|
|
41
|
+
> "Operator mode needs the Chrome MCP server (`@anthropic/claude-in-chrome` or equivalent). Without it, the skill can't drive the admin SPA programmatically. Two options:
|
|
42
|
+
> 1. Install Chrome MCP (Claude Code → MCP settings → add the chrome connector), then re-run `/integrate-feedback`.
|
|
43
|
+
> 2. Provision manually via `docs/INTEGRATING.md` (operator-side: open the admin SPA, create the project, mint the key) and then re-run `/integrate-feedback` in consumer mode with the resulting handoff payload."
|
|
44
|
+
|
|
45
|
+
Stop. Don't continue operator mode without MCP.
|
|
46
|
+
|
|
47
|
+
Consumer mode (install) does NOT need Chrome MCP for the install itself — only for the live smoke test in `references/verify-install.md`. If MCP isn't available, the skill can still do everything up to step 9 of consumer-install.md, and the smoke test degrades to "open this URL in your browser yourself and confirm the FAB renders."
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Operator mode — provisioning
|
|
52
|
+
|
|
53
|
+
**Goal:** end-to-end, produce a Markdown "handoff payload" the teammate can paste into Claude Code (or follow manually) to install the widget.
|
|
54
|
+
|
|
55
|
+
**Read** the full step-by-step procedure at:
|
|
56
|
+
|
|
57
|
+
- `references/operator-provision.md`
|
|
58
|
+
|
|
59
|
+
The procedure walks through:
|
|
60
|
+
|
|
61
|
+
1. Confirming the company exists (or creating it)
|
|
62
|
+
2. Collecting the project name, slug, and `allowed_origins` (the teammate's deployment URL[s])
|
|
63
|
+
3. Driving Chrome MCP to the admin SPA at https://software-factory-3tbbu.ondigitalocean.app
|
|
64
|
+
4. Calling the DRF API from the *already-authenticated* browser context (no token copy-paste)
|
|
65
|
+
5. Minting a `pk_proj_…` widget key and capturing its plaintext value
|
|
66
|
+
6. Assembling and printing the handoff payload
|
|
67
|
+
|
|
68
|
+
**Crucial:** the operator must already be SSO-logged-in to software-factory before the skill drives Chrome. If they're not, the skill detects this and walks them through `/login` first.
|
|
69
|
+
|
|
70
|
+
After the operator branch finishes, ask whether they want to continue into consumer mode in the same conversation (helpful when they're integrating into one of their own apps).
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Consumer mode — installing
|
|
75
|
+
|
|
76
|
+
**Goal:** end-to-end, the widget loads on the host app and a smoke-test report lands in the admin.
|
|
77
|
+
|
|
78
|
+
**Read** the framework-aware install procedure at:
|
|
79
|
+
|
|
80
|
+
- `references/consumer-install.md`
|
|
81
|
+
|
|
82
|
+
The procedure walks through:
|
|
83
|
+
|
|
84
|
+
1. Verifying we're in a JS/TS project (reads `package.json`)
|
|
85
|
+
2. Detecting the framework (Vite/Next/Remix/Vue/SvelteKit/CDN)
|
|
86
|
+
3. Accepting the handoff payload — either pasted verbatim or via `--from-clipboard`
|
|
87
|
+
4. Running `npx @mhosaic/feedback-cli init --api-key=… --endpoint=… --yes` (non-interactive)
|
|
88
|
+
5. For non-Vite-React frameworks: pasting the framework-specific snippet from `references/consumer-install-<framework>.md` and asking the user to apply it
|
|
89
|
+
6. Wiring `fb.identify()` against the consumer's auth provider — recipes in `references/identify-snippets.md`
|
|
90
|
+
7. Optionally wiring `withErrorTracking` + `withReplay` + `withWebVitals`
|
|
91
|
+
8. Running `mhosaic-feedback verify --origin <consumer dev origin>` to confirm CORS, key, and endpoint
|
|
92
|
+
9. Starting the consumer's dev server and driving Chrome to verify the FAB renders
|
|
93
|
+
10. Submitting a `[INTEGRATE-FEEDBACK SMOKE TEST]` synthetic report from the FAB
|
|
94
|
+
11. Driving Chrome to software-factory's `/reports` and confirming the smoke-test report appears
|
|
95
|
+
|
|
96
|
+
The smoke-test workflow lives at:
|
|
97
|
+
|
|
98
|
+
- `references/verify-install.md`
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Principles to follow
|
|
103
|
+
|
|
104
|
+
- **Confirm at every fork.** Never assume a default. Use `AskUserQuestion` for choices, plain text questions for free-form input.
|
|
105
|
+
- **Show, don't tell.** When you can run a command and show output, run it. When you can drive Chrome and screenshot, drive Chrome.
|
|
106
|
+
- **Idempotent and reversible.** Every state-changing step has a stated rollback (e.g. revoke key in admin SPA, run `mhosaic-feedback eject`). Surface that to the user.
|
|
107
|
+
- **Stop on first red.** If the smoke test fails, do not silently retry. Read `references/verify-install.md`'s diagnostic table, narrow the cause, fix it, then re-run from the failing step.
|
|
108
|
+
- **Don't paste secret keys into git.** Walk the user through adding `.env.local` to `.gitignore` (CLI does this automatically; double-check).
|
|
109
|
+
- **Don't claim "done" without proof.** End the workflow with a screenshot of the smoke-test report landing in admin OR with a clearly enumerated red checklist of what's still broken.
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Operator → consumer handoff payload format
|
|
114
|
+
|
|
115
|
+
Always produce this Markdown block at the end of operator mode. The consumer pastes it as the very first message to Claude after running `/integrate-feedback` in their own repo:
|
|
116
|
+
|
|
117
|
+
````markdown
|
|
118
|
+
## Mhosaic Feedback handoff
|
|
119
|
+
|
|
120
|
+
**Project slug:** `your-project-slug`
|
|
121
|
+
**Endpoint:** `https://software-factory-3tbbu.ondigitalocean.app`
|
|
122
|
+
**Public widget key:** `pk_proj_xxxxxxxxxxxxxxxxxxxxxxxxxx`
|
|
123
|
+
**Allowed origin(s):** `https://your-app.example.com`, `http://localhost:5173`
|
|
124
|
+
|
|
125
|
+
To install:
|
|
126
|
+
```bash
|
|
127
|
+
npx @mhosaic/feedback-cli@latest init \
|
|
128
|
+
--api-key pk_proj_xxxxxxxxxxxxxxxxxxxxxxxxxx \
|
|
129
|
+
--endpoint https://software-factory-3tbbu.ondigitalocean.app \
|
|
130
|
+
--yes
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Then verify:
|
|
134
|
+
```bash
|
|
135
|
+
npx @mhosaic/feedback-cli@latest verify \
|
|
136
|
+
--origin http://localhost:5173 \
|
|
137
|
+
--with-test-report
|
|
138
|
+
```
|
|
139
|
+
````
|
|
140
|
+
|
|
141
|
+
The skill's consumer mode parses this block automatically when pasted as input.
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## Skill files
|
|
146
|
+
|
|
147
|
+
```
|
|
148
|
+
skills/integrate-feedback/
|
|
149
|
+
├── SKILL.md # this file (mode router)
|
|
150
|
+
├── references/
|
|
151
|
+
│ ├── operator-provision.md # Chrome MCP → admin SPA → DRF
|
|
152
|
+
│ ├── consumer-install.md # framework router
|
|
153
|
+
│ ├── consumer-install-vite.md # Vite + React (CLI auto-wires)
|
|
154
|
+
│ ├── consumer-install-next.md # Next.js App Router + Pages
|
|
155
|
+
│ ├── consumer-install-nuxt.md # Nuxt 3 (client plugin)
|
|
156
|
+
│ ├── consumer-install-astro.md # Astro (<script> in Layout)
|
|
157
|
+
│ ├── consumer-install-remix.md # Remix root.tsx
|
|
158
|
+
│ ├── consumer-install-vue.md # Vue 3 + Vite
|
|
159
|
+
│ ├── consumer-install-svelte.md # SvelteKit
|
|
160
|
+
│ ├── consumer-install-plain.md # CDN <script> tag
|
|
161
|
+
│ ├── identify-snippets.md # fb.identify() per auth provider
|
|
162
|
+
│ └── verify-install.md # smoke test + diagnostics
|
|
163
|
+
```
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Consumer install — Astro
|
|
2
|
+
|
|
3
|
+
The CLI's auto-wrap is React-only; for Astro you'll wire the widget into a client-only Astro component (or via the renderer of your choice).
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Step 1 — Env vars
|
|
8
|
+
|
|
9
|
+
Astro uses Vite under the hood. Env vars meant for the browser need the `PUBLIC_` prefix:
|
|
10
|
+
|
|
11
|
+
```diff
|
|
12
|
+
- VITE_FEEDBACK_API_KEY=pk_proj_…
|
|
13
|
+
- VITE_FEEDBACK_ENDPOINT=https://…
|
|
14
|
+
+ PUBLIC_FEEDBACK_API_KEY=pk_proj_…
|
|
15
|
+
+ PUBLIC_FEEDBACK_ENDPOINT=https://software-factory-3tbbu.ondigitalocean.app
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Step 2 — Add a client-only widget mount
|
|
21
|
+
|
|
22
|
+
Astro doesn't ship JS by default; you have to opt-in with `client:*` directives or via `<script>` blocks. The cleanest path is a `<script>` block in your shared layout:
|
|
23
|
+
|
|
24
|
+
```astro
|
|
25
|
+
---
|
|
26
|
+
// src/layouts/Layout.astro
|
|
27
|
+
const apiKey = import.meta.env.PUBLIC_FEEDBACK_API_KEY
|
|
28
|
+
const endpoint = import.meta.env.PUBLIC_FEEDBACK_ENDPOINT
|
|
29
|
+
---
|
|
30
|
+
<html>
|
|
31
|
+
<head><slot name="head" /></head>
|
|
32
|
+
<body>
|
|
33
|
+
<slot />
|
|
34
|
+
|
|
35
|
+
<script>
|
|
36
|
+
import { createFeedback } from '@mhosaic/feedback'
|
|
37
|
+
import { withErrorTracking } from '@mhosaic/feedback/error-tracking'
|
|
38
|
+
import { withReplay } from '@mhosaic/feedback/replay'
|
|
39
|
+
import { withWebVitals } from '@mhosaic/feedback/webvitals'
|
|
40
|
+
|
|
41
|
+
const apiKey = import.meta.env.PUBLIC_FEEDBACK_API_KEY
|
|
42
|
+
const endpoint = import.meta.env.PUBLIC_FEEDBACK_ENDPOINT
|
|
43
|
+
if (apiKey && endpoint) {
|
|
44
|
+
withErrorTracking(withReplay(withWebVitals(createFeedback({
|
|
45
|
+
apiKey, endpoint, env: import.meta.env.PROD ? 'prod' : 'dev',
|
|
46
|
+
}))))
|
|
47
|
+
}
|
|
48
|
+
</script>
|
|
49
|
+
</body>
|
|
50
|
+
</html>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Astro will bundle the script and run it on the client. The widget mounts to its own Shadow DOM, independent of Astro's island hydration.
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Step 3 — Wire identify()
|
|
58
|
+
|
|
59
|
+
How you do this depends on what auth backs your Astro site:
|
|
60
|
+
|
|
61
|
+
- **Astro auth integration (e.g. `@auth/astro`, Lucia)** — call `fb.identify()` from a client-only component that reads the session via the matching SDK.
|
|
62
|
+
- **Server-rendered session passed to client via cookies / `window.ENV` pattern** — same trick as Remix: in `Layout.astro` render a small inline `<script>` that surfaces the user via `window.__USER`, then call `fb.identify(window.__USER)` from the import-script block.
|
|
63
|
+
- **Anonymous-only / mostly-static site** — use the anonymous snippet from `references/identify-snippets.md` so the FAB renders for everyone.
|
|
64
|
+
|
|
65
|
+
If the site is mostly static (marketing, docs), anonymous is the typical pick — visitors aren't logged in.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Gotchas
|
|
70
|
+
|
|
71
|
+
- **No SSR window:** Astro's component code runs on the server. Only code inside `<script>` blocks (or `client:*`-hydrated components) runs in the browser. Always wrap the widget init inside `<script>`.
|
|
72
|
+
- **View Transitions API:** if you use Astro's `<ViewTransitions />`, the widget mount survives navigations (it's outside Astro's routed area, in `<body>`). Good — no re-mount churn.
|
|
73
|
+
- **Build output type:** the widget needs `output: 'server'` or `output: 'hybrid'` only if you want the SSR-side env injection; for static (`output: 'static'`), env vars are inlined at build time and the widget still works.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Verifying
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
npx @mhosaic/feedback-cli@latest verify --origin http://localhost:4321 --with-test-report
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Astro dev defaults to port 4321.
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# Consumer install — Next.js
|
|
2
|
+
|
|
3
|
+
The CLI's auto-wrap is Vite+React only. For Next.js you'll paste the snippet manually. Detect whether the user has the **App Router** (`app/` directory) or the **Pages Router** (`pages/` directory) and branch.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Step 1 — Rename env vars
|
|
8
|
+
|
|
9
|
+
The CLI wrote `VITE_FEEDBACK_API_KEY` + `VITE_FEEDBACK_ENDPOINT`. Next.js needs the `NEXT_PUBLIC_` prefix for client-readable env vars. Open `.env.local` and rename both keys:
|
|
10
|
+
|
|
11
|
+
```diff
|
|
12
|
+
- VITE_FEEDBACK_API_KEY=pk_proj_…
|
|
13
|
+
- VITE_FEEDBACK_ENDPOINT=https://…
|
|
14
|
+
+ NEXT_PUBLIC_FEEDBACK_API_KEY=pk_proj_…
|
|
15
|
+
+ NEXT_PUBLIC_FEEDBACK_ENDPOINT=https://…
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Use the `Edit` tool to make the change.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Step 2 — App Router (Next.js 13+) — paste the client component
|
|
23
|
+
|
|
24
|
+
Create a new file `app/components/FeedbackMount.tsx`:
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
'use client'
|
|
28
|
+
|
|
29
|
+
import { useEffect } from 'react'
|
|
30
|
+
import { createFeedback } from '@mhosaic/feedback'
|
|
31
|
+
import { withErrorTracking } from '@mhosaic/feedback/error-tracking'
|
|
32
|
+
import { withReplay } from '@mhosaic/feedback/replay'
|
|
33
|
+
import { withWebVitals } from '@mhosaic/feedback/webvitals'
|
|
34
|
+
|
|
35
|
+
export function FeedbackMount() {
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
const apiKey = process.env.NEXT_PUBLIC_FEEDBACK_API_KEY
|
|
38
|
+
const endpoint = process.env.NEXT_PUBLIC_FEEDBACK_ENDPOINT
|
|
39
|
+
if (!apiKey || !endpoint) return
|
|
40
|
+
|
|
41
|
+
withErrorTracking(
|
|
42
|
+
withReplay(
|
|
43
|
+
withWebVitals(createFeedback({ apiKey, endpoint, env: process.env.NODE_ENV === 'development' ? 'dev' : 'prod' }))
|
|
44
|
+
)
|
|
45
|
+
)
|
|
46
|
+
}, [])
|
|
47
|
+
return null
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Then mount it in `app/layout.tsx` (a Server Component):
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
import { FeedbackMount } from './components/FeedbackMount'
|
|
55
|
+
|
|
56
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
57
|
+
return (
|
|
58
|
+
<html>
|
|
59
|
+
<body>
|
|
60
|
+
<FeedbackMount />
|
|
61
|
+
{children}
|
|
62
|
+
</body>
|
|
63
|
+
</html>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The `'use client'` directive in `FeedbackMount.tsx` is critical — it tells Next.js to ship this component to the browser. Without it, the widget never mounts (the effect never runs on the server).
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Step 3 — Pages Router (Next.js 12 or App-Router-free 13+)
|
|
73
|
+
|
|
74
|
+
Edit `pages/_app.tsx`:
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
import { useEffect } from 'react'
|
|
78
|
+
import type { AppProps } from 'next/app'
|
|
79
|
+
import { createFeedback } from '@mhosaic/feedback'
|
|
80
|
+
import { withErrorTracking } from '@mhosaic/feedback/error-tracking'
|
|
81
|
+
import { withReplay } from '@mhosaic/feedback/replay'
|
|
82
|
+
import { withWebVitals } from '@mhosaic/feedback/webvitals'
|
|
83
|
+
|
|
84
|
+
export default function App({ Component, pageProps }: AppProps) {
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
const apiKey = process.env.NEXT_PUBLIC_FEEDBACK_API_KEY
|
|
87
|
+
const endpoint = process.env.NEXT_PUBLIC_FEEDBACK_ENDPOINT
|
|
88
|
+
if (!apiKey || !endpoint) return
|
|
89
|
+
withErrorTracking(withReplay(withWebVitals(createFeedback({
|
|
90
|
+
apiKey, endpoint, env: process.env.NODE_ENV === 'development' ? 'dev' : 'prod'
|
|
91
|
+
}))))
|
|
92
|
+
}, [])
|
|
93
|
+
return <Component {...pageProps} />
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Step 4 — Wire identify()
|
|
100
|
+
|
|
101
|
+
See `references/identify-snippets.md`. Most Next.js apps use NextAuth or Clerk; both snippets there assume a client component with `useSession()` / `useUser()`.
|
|
102
|
+
|
|
103
|
+
If you're using NextAuth, the cleanest pattern is to add a `<FeedbackIdentity />` client component as a sibling of `<FeedbackMount />` in `app/layout.tsx`.
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Gotchas
|
|
108
|
+
|
|
109
|
+
- **Server Components can't mount the widget.** Any module that touches the DOM (the widget's mount routine, rrweb's MutationObserver) must run on the client. Either wrap in `'use client'` or call inside a `useEffect`.
|
|
110
|
+
- **Static generation:** if you use `getStaticProps` or App Router's static rendering, the widget's identity will only attach on hydration. Server-rendered pages will briefly show *without* the FAB until the client effect fires. That's expected.
|
|
111
|
+
- **Edge runtime:** the widget assumes `window` exists. Don't import it from anything that runs on the Edge runtime (middleware, route handlers with `runtime = 'edge'`).
|
|
112
|
+
- **CSP:** if your `next.config.js` sets a strict CSP, you'll need to allow `connect-src` to the feedback endpoint and `worker-src 'blob:'` for rrweb. The widget docs cover this; cite them when CSP errors appear in DevTools.
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Env vars
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
NEXT_PUBLIC_FEEDBACK_API_KEY=pk_proj_…
|
|
120
|
+
NEXT_PUBLIC_FEEDBACK_ENDPOINT=https://software-factory-3tbbu.ondigitalocean.app
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`NEXT_PUBLIC_*` are inlined at build time. To change them, restart `next dev`.
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Verifying
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
npx @mhosaic/feedback-cli@latest verify --origin http://localhost:3000 --with-test-report
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
The default Next.js dev port is 3000 (not Vite's 5173). Adjust `--origin` to match.
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Consumer install — Nuxt 3
|
|
2
|
+
|
|
3
|
+
The CLI's auto-wrap is React-only; for Nuxt you paste the snippet into a client-only plugin.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Step 1 — Env vars
|
|
8
|
+
|
|
9
|
+
Nuxt 3 exposes env vars via `runtimeConfig`. Open `.env` (or `.env.local` — Nuxt reads both) and rename:
|
|
10
|
+
|
|
11
|
+
```diff
|
|
12
|
+
- VITE_FEEDBACK_API_KEY=pk_proj_…
|
|
13
|
+
- VITE_FEEDBACK_ENDPOINT=https://…
|
|
14
|
+
+ NUXT_PUBLIC_FEEDBACK_API_KEY=pk_proj_…
|
|
15
|
+
+ NUXT_PUBLIC_FEEDBACK_ENDPOINT=https://software-factory-3tbbu.ondigitalocean.app
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Add the matching `runtimeConfig.public` entries to `nuxt.config.ts`:
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
export default defineNuxtConfig({
|
|
22
|
+
runtimeConfig: {
|
|
23
|
+
public: {
|
|
24
|
+
feedbackApiKey: '',
|
|
25
|
+
feedbackEndpoint: '',
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
})
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Nuxt auto-binds `NUXT_PUBLIC_*` env vars to the matching `runtimeConfig.public.*` keys at runtime.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Step 2 — Add a client-only plugin
|
|
36
|
+
|
|
37
|
+
Create `plugins/feedback.client.ts` (the `.client.ts` suffix tells Nuxt to bundle this only for the browser, not SSR):
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
import { createFeedback } from '@mhosaic/feedback'
|
|
41
|
+
import { withErrorTracking } from '@mhosaic/feedback/error-tracking'
|
|
42
|
+
import { withReplay } from '@mhosaic/feedback/replay'
|
|
43
|
+
import { withWebVitals } from '@mhosaic/feedback/webvitals'
|
|
44
|
+
|
|
45
|
+
export default defineNuxtPlugin(() => {
|
|
46
|
+
const config = useRuntimeConfig()
|
|
47
|
+
const fb = withErrorTracking(
|
|
48
|
+
withReplay(
|
|
49
|
+
withWebVitals(createFeedback({
|
|
50
|
+
apiKey: config.public.feedbackApiKey as string,
|
|
51
|
+
endpoint: config.public.feedbackEndpoint as string,
|
|
52
|
+
env: process.dev ? 'dev' : 'prod',
|
|
53
|
+
}))
|
|
54
|
+
)
|
|
55
|
+
)
|
|
56
|
+
return { provide: { feedback: fb } }
|
|
57
|
+
})
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Now `useNuxtApp().$feedback` is available across the app.
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Step 3 — Wire identify()
|
|
65
|
+
|
|
66
|
+
Nuxt's auth is whatever you bolt on (sidebase/nuxt-auth, custom, Supabase, etc.). Use the matching snippet from `references/identify-snippets.md`, called from a `watchEffect` inside `app.vue` or a top-level `<script setup>` that re-runs on auth state changes:
|
|
67
|
+
|
|
68
|
+
```vue
|
|
69
|
+
<!-- app.vue -->
|
|
70
|
+
<script setup lang="ts">
|
|
71
|
+
import { useAuthStore } from '~/stores/auth' // adapt to your auth source
|
|
72
|
+
|
|
73
|
+
const auth = useAuthStore()
|
|
74
|
+
const { $feedback } = useNuxtApp()
|
|
75
|
+
|
|
76
|
+
watchEffect(() => {
|
|
77
|
+
if (auth.user) {
|
|
78
|
+
$feedback.identify({ id: auth.user.id, email: auth.user.email, name: auth.user.name })
|
|
79
|
+
} else {
|
|
80
|
+
$feedback.identify({ id: '', email: '', name: '' })
|
|
81
|
+
}
|
|
82
|
+
})
|
|
83
|
+
</script>
|
|
84
|
+
|
|
85
|
+
<template>
|
|
86
|
+
<NuxtLayout>
|
|
87
|
+
<NuxtPage />
|
|
88
|
+
</NuxtLayout>
|
|
89
|
+
</template>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Gotchas
|
|
95
|
+
|
|
96
|
+
- **SSR + window:** Nuxt SSRs everything by default. The `.client.ts` suffix on the plugin ensures the widget code never runs on the Nitro server (where `window` doesn't exist).
|
|
97
|
+
- **`useRuntimeConfig()` returns empty strings on first render** if the env vars weren't set — Nuxt warns at startup. Verify your `.env` has the right `NUXT_PUBLIC_*` keys.
|
|
98
|
+
- **Static site mode (`nuxt generate`):** the widget hydrates at runtime, same as Next.js. Generated pages render without the FAB until JS hydrates.
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Verifying
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
npx @mhosaic/feedback-cli@latest verify --origin http://localhost:3000 --with-test-report
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Nuxt dev defaults to port 3000.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Consumer install — plain HTML / CDN (no bundler)
|
|
2
|
+
|
|
3
|
+
For host apps without a JS bundler (Django templates, Rails ERB, Hugo, plain `.html` files served statically, etc.), use the CDN-hosted IIFE bundle. No npm install needed.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Step 1 — Find the latest version + SRI hash
|
|
8
|
+
|
|
9
|
+
The bundle is published to jsDelivr from the npm release. Get the current version + integrity hash from the top-level `README.md` of the feedback-tool-mhosaic repo (or from the operator). The format is:
|
|
10
|
+
|
|
11
|
+
```html
|
|
12
|
+
<script
|
|
13
|
+
src="https://cdn.jsdelivr.net/npm/@mhosaic/feedback@<VERSION>/dist/embed.min.js"
|
|
14
|
+
integrity="sha384-<HASH>"
|
|
15
|
+
crossorigin="anonymous"
|
|
16
|
+
data-key="pk_proj_…"
|
|
17
|
+
data-endpoint="https://software-factory-3tbbu.ondigitalocean.app"
|
|
18
|
+
data-env="prod"
|
|
19
|
+
defer
|
|
20
|
+
></script>
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Pin the **exact** version. Don't use `@latest` — that breaks SRI and gives you no rollback story. The operator can update the version on a deploy cycle.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Step 2 — Insert into your template
|
|
28
|
+
|
|
29
|
+
Drop the `<script>` tag near the closing `</body>` of every page that should show the FAB. For Django: `templates/base.html`. For Rails: `app/views/layouts/application.html.erb`. For static sites: the page template / partial that's shared across pages.
|
|
30
|
+
|
|
31
|
+
If your app has a Content Security Policy, you'll need to allow:
|
|
32
|
+
- `script-src https://cdn.jsdelivr.net 'sha384-<HASH>'`
|
|
33
|
+
- `connect-src https://software-factory-3tbbu.ondigitalocean.app`
|
|
34
|
+
- `worker-src 'blob:'` (only if you'll later upgrade to a version that ships rrweb replay)
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Step 3 — Wire identify() via the queue API
|
|
39
|
+
|
|
40
|
+
The CDN bundle exposes a global function `window.Feedback` that queues calls until the bundle finishes loading. Drop this **before** the `<script src=…>` tag so the queue exists when the bundle initializes:
|
|
41
|
+
|
|
42
|
+
```html
|
|
43
|
+
<script>
|
|
44
|
+
window.Feedback = window.Feedback || function () {
|
|
45
|
+
(window.Feedback.q = window.Feedback.q || []).push(arguments)
|
|
46
|
+
}
|
|
47
|
+
</script>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Then, anywhere after your auth resolves, call:
|
|
51
|
+
|
|
52
|
+
```html
|
|
53
|
+
<script>
|
|
54
|
+
Feedback('identify', {
|
|
55
|
+
id: '<%= current_user.id %>',
|
|
56
|
+
email: '<%= current_user.email %>',
|
|
57
|
+
name: '<%= current_user.name %>',
|
|
58
|
+
})
|
|
59
|
+
</script>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
(Substitute your template language's interpolation: `{{ }}` for Django, `<%= %>` for ERB, etc.)
|
|
63
|
+
|
|
64
|
+
For anonymous-only deployments, pass a stable placeholder id so the FAB still renders:
|
|
65
|
+
|
|
66
|
+
```html
|
|
67
|
+
<script>
|
|
68
|
+
Feedback('identify', { id: 'anon-' + (localStorage.getItem('anonId') || (localStorage.setItem('anonId', crypto.randomUUID()), localStorage.getItem('anonId'))), name: 'Anonymous' })
|
|
69
|
+
</script>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Gotchas
|
|
75
|
+
|
|
76
|
+
- **No auto-capture modules on the CDN bundle.** The CDN IIFE is the bare widget. `withErrorTracking` / `withReplay` / `withWebVitals` are available only via npm import. If the user wants those, they must switch to the bundled (npm) install — drop the `<script>` tag and follow the framework-specific reference instead.
|
|
77
|
+
- **SRI hash must match the bytes** at the pinned version. If you bump the version, regenerate the hash:
|
|
78
|
+
```bash
|
|
79
|
+
curl -s https://cdn.jsdelivr.net/npm/@mhosaic/feedback@<VERSION>/dist/embed.min.js | openssl dgst -sha384 -binary | openssl base64 -A
|
|
80
|
+
```
|
|
81
|
+
- **CDN cache lag:** new versions can take up to an hour to propagate across jsDelivr's edge. If you just published and the version 404s, wait or use `https://cdn.jsdelivr.net/npm/@mhosaic/feedback/dist/embed.min.js` (without `@VERSION`, but breaks SRI) for a one-off test, then pin properly.
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Verifying
|
|
86
|
+
|
|
87
|
+
The `verify` CLI command reads `.env.local`, which the CDN install doesn't use. To verify the CDN install, drive Chrome to your page and check:
|
|
88
|
+
|
|
89
|
+
1. The `<script>` tag loads (DevTools network → no 404 / SRI error)
|
|
90
|
+
2. `window.Feedback` exists (DevTools console: `typeof Feedback`)
|
|
91
|
+
3. The FAB renders (visual check; bottom-right corner)
|
|
92
|
+
4. Submitting a test report lands in `/reports` (see `references/verify-install.md`)
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Consumer install — Remix
|
|
2
|
+
|
|
3
|
+
The CLI's auto-wrap is Vite+React only; Remix needs the snippet pasted manually.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Step 1 — Rename env vars
|
|
8
|
+
|
|
9
|
+
Remix exposes env vars to the client via a loader on the root route, not a build-time prefix. Open `.env.local`, **drop the `VITE_` prefix** (you'll surface these through a loader instead):
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
FEEDBACK_API_KEY=pk_proj_…
|
|
13
|
+
FEEDBACK_ENDPOINT=https://software-factory-3tbbu.ondigitalocean.app
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Step 2 — Expose env via the root loader
|
|
19
|
+
|
|
20
|
+
In `app/root.tsx`, add a loader that surfaces the values to the client window:
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
import type { LoaderFunctionArgs } from '@remix-run/node'
|
|
24
|
+
import { json, useLoaderData, Outlet, Scripts } from '@remix-run/react'
|
|
25
|
+
import { useEffect } from 'react'
|
|
26
|
+
import { createFeedback } from '@mhosaic/feedback'
|
|
27
|
+
import { withErrorTracking } from '@mhosaic/feedback/error-tracking'
|
|
28
|
+
import { withReplay } from '@mhosaic/feedback/replay'
|
|
29
|
+
import { withWebVitals } from '@mhosaic/feedback/webvitals'
|
|
30
|
+
|
|
31
|
+
export async function loader({ request }: LoaderFunctionArgs) {
|
|
32
|
+
return json({
|
|
33
|
+
ENV: {
|
|
34
|
+
FEEDBACK_API_KEY: process.env.FEEDBACK_API_KEY,
|
|
35
|
+
FEEDBACK_ENDPOINT: process.env.FEEDBACK_ENDPOINT,
|
|
36
|
+
},
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export default function App() {
|
|
41
|
+
const data = useLoaderData<typeof loader>()
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
const { FEEDBACK_API_KEY, FEEDBACK_ENDPOINT } = data.ENV
|
|
44
|
+
if (!FEEDBACK_API_KEY || !FEEDBACK_ENDPOINT) return
|
|
45
|
+
withErrorTracking(withReplay(withWebVitals(createFeedback({
|
|
46
|
+
apiKey: FEEDBACK_API_KEY,
|
|
47
|
+
endpoint: FEEDBACK_ENDPOINT,
|
|
48
|
+
env: process.env.NODE_ENV === 'development' ? 'dev' : 'prod',
|
|
49
|
+
}))))
|
|
50
|
+
}, [data.ENV])
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<html>
|
|
54
|
+
<head><title>Your app</title></head>
|
|
55
|
+
<body>
|
|
56
|
+
<Outlet />
|
|
57
|
+
<script
|
|
58
|
+
dangerouslySetInnerHTML={{
|
|
59
|
+
__html: `window.ENV = ${JSON.stringify(data.ENV)}`,
|
|
60
|
+
}}
|
|
61
|
+
/>
|
|
62
|
+
<Scripts />
|
|
63
|
+
</body>
|
|
64
|
+
</html>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The `window.ENV` script tag is Remix's idiomatic env-passing pattern. If your app already has a root loader doing this, just add the two FEEDBACK_* keys to the existing `ENV` object instead of duplicating the loader.
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Step 3 — Wire identify()
|
|
74
|
+
|
|
75
|
+
See `references/identify-snippets.md`. Remix apps usually authenticate via cookies + a loader-returned user; the identify call belongs in a `useEffect` on whichever route boundary returns the user object (usually root).
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Gotchas
|
|
80
|
+
|
|
81
|
+
- **Loader runs on every request.** Putting the widget mount in `useEffect` with `[data.ENV]` as a dep ensures it runs once on the client and not on re-renders.
|
|
82
|
+
- **SSR + rrweb:** rrweb touches `MutationObserver` and `document`; never import the widget at module-top-level — keep it inside the `useEffect`. The pattern above does that.
|
|
83
|
+
- **Catch boundaries:** if your root route errors, the widget won't mount. That's fine — errors will be visible from the error UI, and once the operator fixes the underlying issue, the widget comes back.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Verifying
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
npx @mhosaic/feedback-cli@latest verify --origin http://localhost:3000 --with-test-report
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Remix dev's default port is 3000.
|