@mhosaic/feedback-cli 0.13.0 → 0.14.0
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 +14 -4
- package/dist/bin.js.map +1 -1
- package/dist/install-skill-CIZ3ZQI7.js +88 -0
- package/dist/install-skill-CIZ3ZQI7.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 +142 -0
- package/skills/integrate-feedback/references/consumer-install-next.md +133 -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 +179 -0
- package/skills/integrate-feedback/references/identify-snippets.md +218 -0
- package/skills/integrate-feedback/references/operator-provision.md +233 -0
- package/skills/integrate-feedback/references/verify-install.md +151 -0
|
@@ -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,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.
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Consumer install — SvelteKit
|
|
2
|
+
|
|
3
|
+
The CLI's auto-wrap is React-only; for SvelteKit you paste the snippet into the root layout.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Step 1 — Env vars
|
|
8
|
+
|
|
9
|
+
SvelteKit uses Vite's env system but with the `PUBLIC_` prefix for client-readable values. Open `.env.local` and rename:
|
|
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://…
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Step 2 — Mount in `+layout.svelte`
|
|
21
|
+
|
|
22
|
+
Edit `src/routes/+layout.svelte`:
|
|
23
|
+
|
|
24
|
+
```svelte
|
|
25
|
+
<script lang="ts">
|
|
26
|
+
import { onMount } from 'svelte'
|
|
27
|
+
import { env } from '$env/dynamic/public'
|
|
28
|
+
import { createFeedback } from '@mhosaic/feedback'
|
|
29
|
+
import { withErrorTracking } from '@mhosaic/feedback/error-tracking'
|
|
30
|
+
import { withReplay } from '@mhosaic/feedback/replay'
|
|
31
|
+
import { withWebVitals } from '@mhosaic/feedback/webvitals'
|
|
32
|
+
|
|
33
|
+
let fb: ReturnType<typeof createFeedback> | undefined
|
|
34
|
+
|
|
35
|
+
onMount(() => {
|
|
36
|
+
const apiKey = env.PUBLIC_FEEDBACK_API_KEY
|
|
37
|
+
const endpoint = env.PUBLIC_FEEDBACK_ENDPOINT
|
|
38
|
+
if (!apiKey || !endpoint) return
|
|
39
|
+
fb = withErrorTracking(
|
|
40
|
+
withReplay(
|
|
41
|
+
withWebVitals(createFeedback({ apiKey, endpoint, env: import.meta.env.PROD ? 'prod' : 'dev' }))
|
|
42
|
+
)
|
|
43
|
+
)
|
|
44
|
+
})
|
|
45
|
+
</script>
|
|
46
|
+
|
|
47
|
+
<slot />
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`onMount` is client-only by definition in Svelte — no SSR import needed.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Step 3 — Wire identify()
|
|
55
|
+
|
|
56
|
+
If your app uses SvelteKit's `+layout.server.ts` to return the user, surface that to the client via the layout `data` and watch it with a `$:` block:
|
|
57
|
+
|
|
58
|
+
```svelte
|
|
59
|
+
<script lang="ts">
|
|
60
|
+
import { page } from '$app/stores'
|
|
61
|
+
// ... existing imports
|
|
62
|
+
|
|
63
|
+
$: if (fb) {
|
|
64
|
+
const user = $page.data.user
|
|
65
|
+
if (user) fb.identify({ id: user.id, email: user.email, name: user.name })
|
|
66
|
+
else fb.identify({ id: '', email: '', name: '' })
|
|
67
|
+
}
|
|
68
|
+
</script>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
For Supabase/Firebase/custom JWT setups, use the snippet from `references/identify-snippets.md` and call it inside the same `onMount` that creates `fb`.
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Gotchas
|
|
76
|
+
|
|
77
|
+
- **SSR + window:** the widget touches `window`. Always init inside `onMount` (or after `if (browser) {…}` from `$app/environment`). Never at module top-level.
|
|
78
|
+
- **`$env/dynamic/public` vs `$env/static/public`:** dynamic reads at runtime (works with multiple deploy targets); static inlines at build (better for tree-shaking). Either works for the widget — pick the one your app already uses.
|
|
79
|
+
- **adapter-static / SSG:** the widget hydrates at runtime, so SSG-rendered pages render without the FAB until JS hydrates. Same caveat as Next.js.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Verifying
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
npx @mhosaic/feedback-cli@latest verify --origin http://localhost:5173 --with-test-report
|
|
87
|
+
```
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# Consumer install — Vite + React
|
|
2
|
+
|
|
3
|
+
The CLI auto-wires this configuration. Your job is to **verify** the auto-wrap landed, not write the snippet yourself.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## After `mhosaic-feedback init` ran
|
|
8
|
+
|
|
9
|
+
The CLI should have:
|
|
10
|
+
1. Installed `@mhosaic/feedback`
|
|
11
|
+
2. Written `.env.local` with `VITE_FEEDBACK_API_KEY=…` and `VITE_FEEDBACK_ENDPOINT=…`
|
|
12
|
+
3. Wrapped `src/main.tsx` with `<FeedbackProvider>` between the marker comments
|
|
13
|
+
|
|
14
|
+
Read `src/main.tsx` and confirm you see:
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
// === mhosaic-feedback:import:start ===
|
|
18
|
+
import { FeedbackProvider } from '@mhosaic/feedback/react'
|
|
19
|
+
// === mhosaic-feedback:import:end ===
|
|
20
|
+
|
|
21
|
+
// ... existing imports
|
|
22
|
+
|
|
23
|
+
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
24
|
+
{/* === mhosaic-feedback:wrap:start === */}
|
|
25
|
+
<FeedbackProvider
|
|
26
|
+
apiKey={import.meta.env.VITE_FEEDBACK_API_KEY}
|
|
27
|
+
endpoint={import.meta.env.VITE_FEEDBACK_ENDPOINT}
|
|
28
|
+
env="prod"
|
|
29
|
+
>
|
|
30
|
+
{/* === mhosaic-feedback:wrap:end === */}
|
|
31
|
+
<App />
|
|
32
|
+
{/* === mhosaic-feedback:wrap:start === */}
|
|
33
|
+
</FeedbackProvider>
|
|
34
|
+
{/* === mhosaic-feedback:wrap:end === */}
|
|
35
|
+
)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
If any of those markers is missing, the auto-wrap failed (rare — usually because the entry file's structure didn't match the regex). Run `npx @mhosaic/feedback-cli@latest doctor` to confirm. If doctor reports a wiring miss, paste the snippet above into `src/main.tsx` manually, or open an issue on the feedback-tool-mhosaic repo.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Optional: auto-capture middleware
|
|
43
|
+
|
|
44
|
+
The default wrap uses `<FeedbackProvider>`, which doesn't expose the wrapper-chain pattern. To add `withErrorTracking` / `withReplay` / `withWebVitals` on a Vite+React project, replace `<FeedbackProvider>` with a manual `createFeedback()` call in a small client-only module:
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
// src/feedback.ts
|
|
48
|
+
import { createFeedback } from '@mhosaic/feedback'
|
|
49
|
+
import { withErrorTracking } from '@mhosaic/feedback/error-tracking'
|
|
50
|
+
import { withReplay } from '@mhosaic/feedback/replay'
|
|
51
|
+
import { withWebVitals } from '@mhosaic/feedback/webvitals'
|
|
52
|
+
|
|
53
|
+
export const fb = withErrorTracking(
|
|
54
|
+
withReplay(
|
|
55
|
+
withWebVitals(createFeedback({
|
|
56
|
+
apiKey: import.meta.env.VITE_FEEDBACK_API_KEY,
|
|
57
|
+
endpoint: import.meta.env.VITE_FEEDBACK_ENDPOINT,
|
|
58
|
+
env: import.meta.env.PROD ? 'prod' : 'dev',
|
|
59
|
+
}))
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Then in `src/main.tsx` replace the `<FeedbackProvider>` wrap with an import of `./feedback` at the top — so the side effect (widget mount + auto-capture) runs once at module load:
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
import './feedback' // mounts the widget + arms error/replay/webvitals
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
And remove the `<FeedbackProvider>` JSX wrap. The widget mounts itself to its own Shadow DOM container; there's no React provider needed unless you want to call `useFeedback()` from components.
|
|
71
|
+
|
|
72
|
+
If you do want `useFeedback()`, keep `<FeedbackProvider>` *and* the manual `createFeedback()` — they cooperate. See `packages/core/src/react/FeedbackProvider.tsx` for the contract.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Env vars
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
VITE_FEEDBACK_API_KEY=pk_proj_…
|
|
80
|
+
VITE_FEEDBACK_ENDPOINT=https://software-factory-3tbbu.ondigitalocean.app
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Vite inlines `import.meta.env.VITE_*` at build time. Don't try to read these at runtime from `process.env` — Vite doesn't expose `process` in browser code.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Gotchas
|
|
88
|
+
|
|
89
|
+
- **`.env.local` is build-time only.** Changes require restarting `pnpm dev`.
|
|
90
|
+
- **Shadow DOM clipping:** the widget mounts to `document.body`. If your app uses `overflow: hidden` on `html` or `body`, the FAB and screen capture may behave oddly. Add a `position: fixed; z-index: 9999;` shim to a root container.
|
|
91
|
+
- **Strict Mode double-effect:** React 18's `<StrictMode>` runs effects twice in dev. The widget is idempotent — second mount is a no-op — but you may see two "widget loaded" console messages in dev. Harmless.
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Verifying
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
npx @mhosaic/feedback-cli@latest doctor
|
|
99
|
+
npx @mhosaic/feedback-cli@latest verify --origin http://localhost:5173 --with-test-report
|
|
100
|
+
```
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Consumer install — Vue 3 + Vite
|
|
2
|
+
|
|
3
|
+
The CLI's auto-wrap is React-only; for Vue you paste the snippet into `src/main.ts`.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Step 1 — Env vars
|
|
8
|
+
|
|
9
|
+
The CLI already wrote Vite-flavored env vars. Vue + Vite uses the same `VITE_*` prefix, so **no rename needed**:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
VITE_FEEDBACK_API_KEY=pk_proj_…
|
|
13
|
+
VITE_FEEDBACK_ENDPOINT=https://software-factory-3tbbu.ondigitalocean.app
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Step 2 — Initialize at app boot
|
|
19
|
+
|
|
20
|
+
Edit `src/main.ts`:
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
import { createApp } from 'vue'
|
|
24
|
+
import App from './App.vue'
|
|
25
|
+
|
|
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
|
+
const fb = withErrorTracking(
|
|
32
|
+
withReplay(
|
|
33
|
+
withWebVitals(createFeedback({
|
|
34
|
+
apiKey: import.meta.env.VITE_FEEDBACK_API_KEY,
|
|
35
|
+
endpoint: import.meta.env.VITE_FEEDBACK_ENDPOINT,
|
|
36
|
+
env: import.meta.env.PROD ? 'prod' : 'dev',
|
|
37
|
+
}))
|
|
38
|
+
)
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
const app = createApp(App)
|
|
42
|
+
// Optional: expose `this.$feedback` in components and `inject('feedback')` in composables
|
|
43
|
+
app.provide('feedback', fb)
|
|
44
|
+
app.mount('#app')
|
|
45
|
+
|
|
46
|
+
// Export for use in other modules (e.g. your auth store)
|
|
47
|
+
export { fb }
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The widget mounts itself to its own Shadow DOM container outside Vue's root. Calling `createFeedback()` once at module load is sufficient — there's no Vue plugin to install.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Step 3 — Wire identify()
|
|
55
|
+
|
|
56
|
+
Use the Supabase / Firebase / custom JWT recipe from `references/identify-snippets.md`. Vue 3's `watchEffect` is the idiomatic place to call `fb.identify()` from a Pinia/Vuex store or a composable:
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
// src/composables/useAuthIdentity.ts
|
|
60
|
+
import { watchEffect } from 'vue'
|
|
61
|
+
import { useAuthStore } from '@/stores/auth'
|
|
62
|
+
import { fb } from '@/main'
|
|
63
|
+
|
|
64
|
+
export function useFeedbackIdentity() {
|
|
65
|
+
const auth = useAuthStore()
|
|
66
|
+
watchEffect(() => {
|
|
67
|
+
if (auth.user) {
|
|
68
|
+
fb.identify({ id: auth.user.id, email: auth.user.email, name: auth.user.name })
|
|
69
|
+
} else {
|
|
70
|
+
fb.identify({ id: '', email: '', name: '' })
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Call `useFeedbackIdentity()` once from a top-level component (e.g. inside `App.vue`'s `<script setup>`).
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Gotchas
|
|
81
|
+
|
|
82
|
+
- **Vue Devtools collisions:** none observed in practice; the widget runs in its own Shadow DOM.
|
|
83
|
+
- **`<KeepAlive>` and route changes:** the widget mounts once at module load and persists across all route changes. You don't need to re-mount it.
|
|
84
|
+
- **Nuxt 3:** if the user is on Nuxt (not bare Vue + Vite), surface that — Nuxt has its own SSR rules and the snippet should go in a `client.ts` plugin (`plugins/feedback.client.ts`) instead of `main.ts`. The `.client.ts` suffix tells Nuxt to bundle it as client-only.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Verifying
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
npx @mhosaic/feedback-cli@latest verify --origin http://localhost:5173 --with-test-report
|
|
92
|
+
```
|