@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.
@@ -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
+ ```