@mhosaic/feedback-cli 0.14.1 → 0.17.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.
Files changed (27) hide show
  1. package/dist/bin.js +3 -3
  2. package/dist/{doctor-HTTAR4ZB.js → doctor-K2C6LBR2.js} +2 -2
  3. package/dist/doctor-K2C6LBR2.js.map +1 -0
  4. package/dist/{init-P45CEXDP.js → init-RS7BKALE.js} +3 -3
  5. package/dist/init-RS7BKALE.js.map +1 -0
  6. package/dist/{install-skill-3OPVFMTK.js → install-skill-QJ4ZDVVR.js} +4 -2
  7. package/dist/{install-skill-3OPVFMTK.js.map → install-skill-QJ4ZDVVR.js.map} +1 -1
  8. package/package.json +1 -1
  9. package/skills/feedback-close/SKILL.md +58 -0
  10. package/skills/feedback-fix/SKILL.md +121 -0
  11. package/skills/feedback-pull/SKILL.md +56 -0
  12. package/skills/feedback-watch-merges/SKILL.md +66 -0
  13. package/skills/integrate-feedback/SKILL.md +100 -75
  14. package/skills/integrate-feedback/references/consumer-install-astro.md +26 -6
  15. package/skills/integrate-feedback/references/consumer-install-next.md +39 -14
  16. package/skills/integrate-feedback/references/consumer-install-nuxt.md +35 -12
  17. package/skills/integrate-feedback/references/consumer-install-plain.md +55 -30
  18. package/skills/integrate-feedback/references/consumer-install-remix.md +32 -7
  19. package/skills/integrate-feedback/references/consumer-install-svelte.md +27 -7
  20. package/skills/integrate-feedback/references/consumer-install-vite.md +23 -14
  21. package/skills/integrate-feedback/references/consumer-install-vue.md +34 -11
  22. package/skills/integrate-feedback/references/consumer-install.md +74 -27
  23. package/skills/integrate-feedback/references/identify-snippets.md +3 -3
  24. package/skills/integrate-feedback/references/operator-provision.md +125 -33
  25. package/skills/issue-pull/SKILL.md +46 -0
  26. package/dist/doctor-HTTAR4ZB.js.map +0 -1
  27. package/dist/init-P45CEXDP.js.map +0 -1
@@ -1,17 +1,17 @@
1
1
  # Consumer install — plain HTML / CDN (no bundler)
2
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.
3
+ For host apps without a JS bundler (Django templates, Rails ERB, Hugo, plain `.html` files served statically, etc.), drop in the CDN-hosted loader. No npm install needed.
4
4
 
5
- ---
5
+ The loader is the **manifest-driven thin client** — it's tiny (~4 KB gzipped) and never changes. The actual widget bundle is fetched at runtime from a Mhosaic-controlled manifest URL, so once you've installed the tag below, **you never edit it again** — Mhosaic publishes a new release row and your next page load picks up the new widget bundle.
6
6
 
7
- ## Step 1 — Find the latest version + SRI hash
7
+ ---
8
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:
9
+ ## Step 1 Insert one script tag, forever
10
10
 
11
11
  ```html
12
12
  <script
13
- src="https://cdn.jsdelivr.net/npm/@mhosaic/feedback@<VERSION>/dist/embed.min.js"
14
- integrity="sha384-<HASH>"
13
+ src="https://cdn.jsdelivr.net/npm/@mhosaic/feedback@1/dist/loader.min.js"
14
+ integrity="sha384-<LOADER_SRI_HASH>"
15
15
  crossorigin="anonymous"
16
16
  data-key="pk_proj_…"
17
17
  data-endpoint="https://software-factory-3tbbu.ondigitalocean.app"
@@ -20,30 +20,34 @@ The bundle is published to jsDelivr from the npm release. Get the current versio
20
20
  ></script>
21
21
  ```
22
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
- ---
23
+ - **Version pin (`@1`):** pin to the LOADER's major version, not a patch version. The loader's API is stable within v1; we'll never break it. Patch updates to the loader propagate via jsDelivr's caches automatically.
24
+ - **SRI:** the loader file is tiny and changes rarely. Get the current SRI from the latest GitHub Release notes for `feedback-tool-mhosaic` (or compute with `curl -s https://cdn.jsdelivr.net/npm/@mhosaic/feedback@1/dist/loader.min.js | openssl dgst -sha384 -binary | openssl base64 -A`).
25
+ - **The widget bundle SRI is NOT in this snippet.** That hash is delivered to the loader at runtime via the manifest endpoint, and the loader injects the widget bundle with its own (current) SRI on the fly. So the bundle is always integrity-checked, but the host never has to update its SRI when the widget version changes.
26
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.
27
+ Drop the 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
28
 
31
29
  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)
30
+
31
+ - `script-src https://cdn.jsdelivr.net` (for both the loader and the widget bundle)
32
+ - `connect-src https://software-factory-3tbbu.ondigitalocean.app` (for the manifest endpoint + report submissions)
33
+ - `worker-src 'blob:'` (only if the operator-enabled replay module is in use)
35
34
 
36
35
  ---
37
36
 
38
- ## Step 3 — Wire identify() via the queue API
37
+ ## Step 2 — Wire identify() via the queue API
39
38
 
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:
39
+ The loader exposes a global `window.Feedback` queue function. Drop this **before** the `<script src=…>` tag so the queue exists when the loader initializes:
41
40
 
42
41
  ```html
43
42
  <script>
44
- window.Feedback = window.Feedback || function () {
45
- (window.Feedback.q = window.Feedback.q || []).push(arguments)
46
- }
43
+ window.Feedback =
44
+ window.Feedback ||
45
+ function () {
46
+ ;(window.Feedback.q = window.Feedback.q || []).push({
47
+ name: arguments[0],
48
+ args: Array.prototype.slice.call(arguments, 1),
49
+ })
50
+ }
47
51
  </script>
48
52
  ```
49
53
 
@@ -65,20 +69,40 @@ For anonymous-only deployments, pass a stable placeholder id so the FAB still re
65
69
 
66
70
  ```html
67
71
  <script>
68
- Feedback('identify', { id: 'anon-' + (localStorage.getItem('anonId') || (localStorage.setItem('anonId', crypto.randomUUID()), localStorage.getItem('anonId'))), name: 'Anonymous' })
72
+ var anonId =
73
+ localStorage.getItem('anonId') ||
74
+ (function () {
75
+ var id = 'anon-' + crypto.randomUUID()
76
+ localStorage.setItem('anonId', id)
77
+ return id
78
+ })()
79
+ Feedback('identify', { id: anonId, name: 'Anonymous' })
69
80
  </script>
70
81
  ```
71
82
 
72
83
  ---
73
84
 
85
+ ## How updates work after install
86
+
87
+ Mhosaic ships a new widget release (new bundle URL + SRI in the Release table). The next time any user loads any page on your site, the loader:
88
+
89
+ 1. Fetches `<endpoint>/api/feedback/v1/widget-manifest/?pk=<your_pk>` (with a 60-second cache header so subsequent page loads in the same minute don't re-fetch).
90
+ 2. Reads the response: `{version, bundle_url, sri_hash}`.
91
+ 3. Injects `<script src="<bundle_url>" integrity="<sri_hash>" crossorigin="anonymous">` — the browser refuses to run the bundle if its bytes don't match.
92
+ 4. Widget mounts. Done.
93
+
94
+ No host action required. No PRs to merge. No SRI hash to update. The manifest endpoint is the trust root (it's on your Mhosaic backend, served over HTTPS), and SRI on the injected bundle protects against jsDelivr compromise between the manifest lookup and the script load.
95
+
96
+ If Mhosaic ever needs to disable the widget on your project (kill switch), they flip `Project.widget_enabled = False` in the admin — the next page load gets `{enabled: false}` and the loader silently bails. Your site doesn't crash.
97
+
98
+ If you need to **pin to a specific widget version** (e.g. before a critical launch), the operator sets `Project.pinned_version = <release>` — your site stays on that exact version regardless of new releases until they unset the pin.
99
+
100
+ ---
101
+
74
102
  ## Gotchas
75
103
 
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.
104
+ - **No auto-capture modules in the CDN install.** `withErrorTracking` / `withReplay` / `withWebVitals` are middleware wrappers that ship via the npm path — and on that path **error-tracking is now on by default** (uncaught errors + unhandled rejections auto-file as synthetic reports). The CDN `<script>` install is the bare manual-submit FAB only; it has no error-tracking. If the client wants automatic error capture, they have to switch to the bundled (npm) install — drop the `<script>` tag and follow the framework-specific reference instead.
105
+ - **CDN cache lag for new LOADER releases:** new loader patch versions can take up to an hour to propagate across jsDelivr's edge. Bundle updates (the actual widget code) are NOT affected by this — they're pinned with a unique URL per version, so caching is per-version.
82
106
 
83
107
  ---
84
108
 
@@ -86,7 +110,8 @@ For anonymous-only deployments, pass a stable placeholder id so the FAB still re
86
110
 
87
111
  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
112
 
89
- 1. The `<script>` tag loads (DevTools network → no 404 / SRI error)
113
+ 1. The loader `<script>` tag loads (DevTools network → no 404 / SRI error)
90
114
  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`)
115
+ 3. The bundle gets fetched (DevTools network a second request to jsDelivr for `widget.min.js`)
116
+ 4. The FAB renders (visual check; bottom-right corner)
117
+ 5. Submitting a test report lands in `/reports` (see `references/verify-install.md`)
@@ -17,16 +17,14 @@ FEEDBACK_ENDPOINT=https://software-factory-3tbbu.ondigitalocean.app
17
17
 
18
18
  ## Step 2 — Expose env via the root loader
19
19
 
20
- In `app/root.tsx`, add a loader that surfaces the values to the client window:
20
+ In `app/root.tsx`, add a loader that surfaces the values to the client window. Error-tracking is **on by default** — the mount wraps `withErrorTracking` so uncaught errors + unhandled rejections auto-file as synthetic reports:
21
21
 
22
22
  ```typescript
23
23
  import type { LoaderFunctionArgs } from '@remix-run/node'
24
24
  import { json, useLoaderData, Outlet, Scripts } from '@remix-run/react'
25
25
  import { useEffect } from 'react'
26
- import { createFeedback } from '@mhosaic/feedback'
26
+ import { createFeedback } from '@mhosaic/feedback/loader'
27
27
  import { withErrorTracking } from '@mhosaic/feedback/error-tracking'
28
- import { withReplay } from '@mhosaic/feedback/replay'
29
- import { withWebVitals } from '@mhosaic/feedback/webvitals'
30
28
 
31
29
  export async function loader({ request }: LoaderFunctionArgs) {
32
30
  return json({
@@ -42,11 +40,11 @@ export default function App() {
42
40
  useEffect(() => {
43
41
  const { FEEDBACK_API_KEY, FEEDBACK_ENDPOINT } = data.ENV
44
42
  if (!FEEDBACK_API_KEY || !FEEDBACK_ENDPOINT) return
45
- withErrorTracking(withReplay(withWebVitals(createFeedback({
43
+ withErrorTracking(createFeedback({
46
44
  apiKey: FEEDBACK_API_KEY,
47
45
  endpoint: FEEDBACK_ENDPOINT,
48
46
  env: process.env.NODE_ENV === 'development' ? 'dev' : 'prod',
49
- }))))
47
+ }))
50
48
  }, [data.ENV])
51
49
 
52
50
  return (
@@ -66,7 +64,34 @@ export default function App() {
66
64
  }
67
65
  ```
68
66
 
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.
67
+ 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.
68
+
69
+ ---
70
+
71
+ ## Optional — add web-vitals / replay
72
+
73
+ `withWebVitals` and `withReplay` stay **opt-in** (heavier / more sensitive). Add them by chaining around the default instance, error-tracking innermost so it arms first. Import the extra wrappers and replace the `withErrorTracking(createFeedback(...))` call in the `useEffect` above with:
74
+
75
+ ```typescript
76
+ import { withReplay } from '@mhosaic/feedback/replay'
77
+ import { withWebVitals } from '@mhosaic/feedback/webvitals'
78
+
79
+ withReplay(
80
+ withWebVitals(
81
+ withErrorTracking(
82
+ createFeedback({
83
+ apiKey: FEEDBACK_API_KEY,
84
+ endpoint: FEEDBACK_ENDPOINT,
85
+ env: process.env.NODE_ENV === 'development' ? 'dev' : 'prod',
86
+ }),
87
+ ),
88
+ ),
89
+ )
90
+ ```
91
+
92
+ Replay (rrweb, ~30 KiB gzip) captures DOM — confirm the privacy posture before enabling it.
93
+
94
+ **Opting out of error-tracking** (collect-on-consent posture): omit the `withErrorTracking` wrap and pass the bare `createFeedback(...)` instance. Note error-tracking POSTs error messages + stack traces — fine for most apps, but flag it if your error strings can carry end-user PII.
70
95
 
71
96
  ---
72
97
 
@@ -19,16 +19,14 @@ SvelteKit uses Vite's env system but with the `PUBLIC_` prefix for client-readab
19
19
 
20
20
  ## Step 2 — Mount in `+layout.svelte`
21
21
 
22
- Edit `src/routes/+layout.svelte`:
22
+ Edit `src/routes/+layout.svelte`. Error-tracking is **on by default** — the mount wraps `withErrorTracking` so uncaught errors + unhandled rejections auto-file as synthetic reports:
23
23
 
24
24
  ```svelte
25
25
  <script lang="ts">
26
26
  import { onMount } from 'svelte'
27
27
  import { env } from '$env/dynamic/public'
28
- import { createFeedback } from '@mhosaic/feedback'
28
+ import { createFeedback } from '@mhosaic/feedback/loader'
29
29
  import { withErrorTracking } from '@mhosaic/feedback/error-tracking'
30
- import { withReplay } from '@mhosaic/feedback/replay'
31
- import { withWebVitals } from '@mhosaic/feedback/webvitals'
32
30
 
33
31
  let fb: ReturnType<typeof createFeedback> | undefined
34
32
 
@@ -37,9 +35,7 @@ Edit `src/routes/+layout.svelte`:
37
35
  const endpoint = env.PUBLIC_FEEDBACK_ENDPOINT
38
36
  if (!apiKey || !endpoint) return
39
37
  fb = withErrorTracking(
40
- withReplay(
41
- withWebVitals(createFeedback({ apiKey, endpoint, env: import.meta.env.PROD ? 'prod' : 'dev' }))
42
- )
38
+ createFeedback({ apiKey, endpoint, env: import.meta.env.PROD ? 'prod' : 'dev' })
43
39
  )
44
40
  })
45
41
  </script>
@@ -51,6 +47,30 @@ Edit `src/routes/+layout.svelte`:
51
47
 
52
48
  ---
53
49
 
50
+ ## Optional — add web-vitals / replay
51
+
52
+ `withWebVitals` and `withReplay` stay **opt-in** (heavier / more sensitive). Add them by chaining around the default instance, error-tracking innermost so it arms first:
53
+
54
+ ```svelte
55
+ <script lang="ts">
56
+ import { withReplay } from '@mhosaic/feedback/replay'
57
+ import { withWebVitals } from '@mhosaic/feedback/webvitals'
58
+
59
+ // inside onMount, replacing the default wrap:
60
+ fb = withReplay(
61
+ withWebVitals(
62
+ withErrorTracking(createFeedback({ apiKey, endpoint, env: import.meta.env.PROD ? 'prod' : 'dev' }))
63
+ )
64
+ )
65
+ </script>
66
+ ```
67
+
68
+ Replay (rrweb, ~30 KiB gzip) captures DOM — confirm the privacy posture before enabling it.
69
+
70
+ **Opting out of error-tracking** (collect-on-consent posture): omit the `withErrorTracking` wrap and assign the bare `createFeedback(...)` instance. Note error-tracking POSTs error messages + stack traces — fine for most apps, but flag it if your error strings can carry end-user PII.
71
+
72
+ ---
73
+
54
74
  ## Step 3 — Wire identify()
55
75
 
56
76
  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:
@@ -7,6 +7,7 @@ The CLI auto-wires this configuration. Your job is to **verify** the auto-wrap l
7
7
  ## After `mhosaic-feedback init` ran
8
8
 
9
9
  The CLI should have:
10
+
10
11
  1. Installed `@mhosaic/feedback`
11
12
  2. Written `.env.local` with `VITE_FEEDBACK_API_KEY=…` and `VITE_FEEDBACK_ENDPOINT=…`
12
13
  3. Wrapped `src/main.tsx` with `<FeedbackProvider>` between the marker comments
@@ -15,7 +16,7 @@ Read `src/main.tsx` and confirm you see:
15
16
 
16
17
  ```typescript
17
18
  // === mhosaic-feedback:import:start ===
18
- import { FeedbackProvider } from '@mhosaic/feedback/react'
19
+ import { FeedbackProvider } from '@mhosaic/feedback/loader/react'
19
20
  // === mhosaic-feedback:import:end ===
20
21
 
21
22
  // ... existing imports
@@ -39,37 +40,45 @@ If any of those markers is missing, the auto-wrap failed (rare — usually becau
39
40
 
40
41
  ---
41
42
 
42
- ## Optional: auto-capture middleware
43
+ ## Auto-capture modules
44
+
45
+ `<FeedbackProvider>` arms **error-tracking by default** — the `errorTracking` prop defaults to `true`, so uncaught errors + unhandled rejections auto-file as synthetic reports with nothing extra to wire. Opt out (collect-on-consent posture) with:
46
+
47
+ ```tsx
48
+ <FeedbackProvider apiKey={apiKey} endpoint={endpoint} errorTracking={false}>
49
+ ```
43
50
 
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:
51
+ To also add **web-vitals / replay** (still opt-in, heavier), drop the provider's auto-wrap and build the instance manually in a small client-only module:
45
52
 
46
53
  ```typescript
47
54
  // src/feedback.ts
48
- import { createFeedback } from '@mhosaic/feedback'
55
+ import { createFeedback } from '@mhosaic/feedback/loader'
49
56
  import { withErrorTracking } from '@mhosaic/feedback/error-tracking'
50
57
  import { withReplay } from '@mhosaic/feedback/replay'
51
58
  import { withWebVitals } from '@mhosaic/feedback/webvitals'
52
59
 
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
- )
60
+ export const fb = withReplay(
61
+ withWebVitals(
62
+ withErrorTracking(
63
+ createFeedback({
64
+ apiKey: import.meta.env.VITE_FEEDBACK_API_KEY,
65
+ endpoint: import.meta.env.VITE_FEEDBACK_ENDPOINT,
66
+ env: import.meta.env.PROD ? 'prod' : 'dev',
67
+ }),
68
+ ),
69
+ ),
61
70
  )
62
71
  ```
63
72
 
64
73
  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
74
 
66
75
  ```typescript
67
- import './feedback' // mounts the widget + arms error/replay/webvitals
76
+ import './feedback' // mounts the widget + arms error/replay/webvitals
68
77
  ```
69
78
 
70
79
  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
80
 
72
- If you do want `useFeedback()`, keep `<FeedbackProvider>` *and* the manual `createFeedback()` — they cooperate. See `packages/core/src/react/FeedbackProvider.tsx` for the contract.
81
+ If you do want `useFeedback()`, keep `<FeedbackProvider>` _and_ the manual `createFeedback()` — they cooperate. See `packages/core/src/react/FeedbackProvider.tsx` for the contract.
73
82
 
74
83
  ---
75
84
 
@@ -17,25 +17,21 @@ VITE_FEEDBACK_ENDPOINT=https://software-factory-3tbbu.ondigitalocean.app
17
17
 
18
18
  ## Step 2 — Initialize at app boot
19
19
 
20
- Edit `src/main.ts`:
20
+ Edit `src/main.ts`. Error-tracking is **on by default** — the boot wraps `withErrorTracking` so uncaught errors + unhandled rejections auto-file as synthetic reports:
21
21
 
22
22
  ```typescript
23
23
  import { createApp } from 'vue'
24
24
  import App from './App.vue'
25
25
 
26
- import { createFeedback } from '@mhosaic/feedback'
26
+ import { createFeedback } from '@mhosaic/feedback/loader'
27
27
  import { withErrorTracking } from '@mhosaic/feedback/error-tracking'
28
- import { withReplay } from '@mhosaic/feedback/replay'
29
- import { withWebVitals } from '@mhosaic/feedback/webvitals'
30
28
 
31
29
  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
- )
30
+ createFeedback({
31
+ apiKey: import.meta.env.VITE_FEEDBACK_API_KEY,
32
+ endpoint: import.meta.env.VITE_FEEDBACK_ENDPOINT,
33
+ env: import.meta.env.PROD ? 'prod' : 'dev',
34
+ }),
39
35
  )
40
36
 
41
37
  const app = createApp(App)
@@ -51,6 +47,33 @@ The widget mounts itself to its own Shadow DOM container outside Vue's root. Cal
51
47
 
52
48
  ---
53
49
 
50
+ ## Optional — add web-vitals / replay
51
+
52
+ `withWebVitals` and `withReplay` stay **opt-in** (heavier / more sensitive). Add them by chaining around the default instance, error-tracking innermost so it arms first:
53
+
54
+ ```typescript
55
+ import { withReplay } from '@mhosaic/feedback/replay'
56
+ import { withWebVitals } from '@mhosaic/feedback/webvitals'
57
+
58
+ const fb = withReplay(
59
+ withWebVitals(
60
+ withErrorTracking(
61
+ createFeedback({
62
+ apiKey: import.meta.env.VITE_FEEDBACK_API_KEY,
63
+ endpoint: import.meta.env.VITE_FEEDBACK_ENDPOINT,
64
+ env: import.meta.env.PROD ? 'prod' : 'dev',
65
+ }),
66
+ ),
67
+ ),
68
+ )
69
+ ```
70
+
71
+ Replay (rrweb, ~30 KiB gzip) captures DOM — confirm the privacy posture before enabling it.
72
+
73
+ **Opting out of error-tracking** (collect-on-consent posture): omit the `withErrorTracking` wrap and assign the bare `createFeedback(...)` instance. Note error-tracking POSTs error messages + stack traces — fine for most apps, but flag it if your error strings can carry end-user PII.
74
+
75
+ ---
76
+
54
77
  ## Step 3 — Wire identify()
55
78
 
56
79
  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:
@@ -4,6 +4,36 @@ You are guiding a teammate through installing the @mhosaic/feedback widget into
4
4
 
5
5
  ---
6
6
 
7
+ ## CRITICAL: API key handling — read this before doing anything
8
+
9
+ The `pk_proj_…` public widget key is **NOT safe to commit to git**. Even though it's a "public" key (it ends up in the browser-side bundle at runtime), committing it is wrong for several concrete reasons:
10
+
11
+ - Git history is permanent. If the key needs rotation (compromise, audit finding, project transfer), you can't actually remove it — every clone of the repo still has it.
12
+ - GitHub's secret scanners flag any `pk_proj_…` literal in a public repo and notify the org. Even private repos can leak via accidental fork or contributor offboarding.
13
+ - The key is tied to a specific project's `allowed_origins` allow-list — if it leaks and gets used from a domain the operator forgot to lock down, you get garbage reports from the wild.
14
+
15
+ **Rules. NO exceptions.**
16
+
17
+ 1. **NEVER commit the API key to any tracked file.** Not `.env`, not `.env.example`, not `config.ts`, not a comment, not a README. Not even "temporarily, I'll rotate later."
18
+ 2. **If the user asks "can I commit this?" — the answer is NO.** Don't rationalize "it's public anyway" or "it's bundled into the JS at build time so what's the harm." Both are true; neither matters. Refer them to this section.
19
+ 3. **If the host's deploy pipeline excludes `.env.local`** (common with Docker builds — Dockerfile `COPY` of `frontend/` typically skips `.env*.local` via `.dockerignore`, or the build context only includes specific files), the key MUST come from **deploy-platform env vars** (DO App Platform > component > Environment Variables; Vercel > Project Settings > Env Vars; etc.), NOT from a tracked file. Pause and tell the user. Do NOT switch to `.env` (which IS tracked) as a workaround.
20
+ 4. **`.env.local` (or framework equivalent) must be in `.gitignore`.** The CLI's `init` command verifies this. If you're editing files manually because the CLI failed, you verify it yourself before writing the key anywhere.
21
+ 5. **The endpoint URL (`https://software-factory-…`) IS safe to commit** — it's a public URL with no auth on its surface. Only the API key matters.
22
+
23
+ **Per-environment-variable destination table** (which file gets the key for which scenario):
24
+
25
+ | Scenario | API key goes in | Tracked by git? |
26
+ | ---------------------------------------------------------- | ---------------------------------------------------------------------------------- | ------------------------------- |
27
+ | Local dev, Vite/Next/Nuxt/etc. without Docker | `.env.local` | No (gitignored) |
28
+ | Local dev with Docker Compose (host-side build) | `.env.local` | No |
29
+ | Production deploy (DO App Platform, Vercel, Netlify, etc.) | Platform env vars UI | No (lives only on the platform) |
30
+ | Docker build where `.env.local` is excluded from context | Platform env vars (passed in at build time via `--build-arg` or runtime injection) | No |
31
+ | CI build for staging/prod | CI secret (GH Actions secrets, etc.), surfaced as env var to the build | No |
32
+
33
+ If a scenario isn't in the table and you're unsure, **stop and ask the operator**. Do not improvise toward a tracked file.
34
+
35
+ ---
36
+
7
37
  ## Step 1 — Parse the handoff payload
8
38
 
9
39
  Ask the user to paste the handoff payload as plain Markdown. Extract:
@@ -19,7 +49,17 @@ If the user can't find the payload, walk them through asking the operator for it
19
49
 
20
50
  ## Step 2 — Classify the host app
21
51
 
22
- `Read` the user's `package.json`. Three branches:
52
+ Before anything else, **confirm the cwd is actually the target.** This is the most common operator footgun: someone runs `/integrate-feedback` inside `feedback-tool-mhosaic` (where the skill is conveniently invokable) and the consumer phase silently writes the widget into our own monorepo. Two cheap checks:
53
+
54
+ - Read `package.json`. If its `name` is `@mhosaic/feedback-tool-mhosaic`, `@mhosaic/feedback`, `@mhosaic/feedback-admin`, `@mhosaic/feedback-cli`, or contains the literal string `mhosaic/feedback`: **STOP**. The user is inside our own monorepo, not their host app. Tell them:
55
+
56
+ > "Looks like we're inside `feedback-tool-mhosaic` itself. The consumer install writes to the current directory; running it here would install the widget into our own monorepo. Either pass `--cwd <target-app-path>` to the CLI, or close Claude and re-open it inside the target app's root, then re-run `/integrate-feedback` and pick 'Install (consumer)'."
57
+
58
+ Wait for the user to confirm they've moved before continuing.
59
+
60
+ - If `package.json` is missing AND the cwd contains `packages/cli/skills/integrate-feedback/` (we're inside the skill's own source), same stop-and-redirect.
61
+
62
+ Now `Read` the user's `package.json`. Three branches:
23
63
 
24
64
  1. **package.json exists with `dependencies`** → continue to Step 3 (framework detection).
25
65
  2. **No package.json, but `index.html` / `templates/` / `app/views/` / similar template directory exists** → this is a non-JS host app (Django, Rails, Hugo, plain HTML/static site, etc.). Skip Steps 3-5 and go directly to **`references/consumer-install-plain.md`** (CDN `<script>` flow). Tell the user: "I see this isn't a JS/TS app — the CDN path is the right one. The npm-package + framework-snippet flow doesn't apply."
@@ -29,22 +69,22 @@ If the user can't find the payload, walk them through asking the operator for it
29
69
 
30
70
  ## Step 3 — Detect the framework
31
71
 
32
- The CLI does framework detection itself, but you should know the result *before* running it so you can decide which post-install snippet (if any) the user needs to apply.
72
+ The CLI does framework detection itself, but you should know the result _before_ running it so you can decide which post-install snippet (if any) the user needs to apply.
33
73
 
34
74
  Read `package.json`'s `dependencies` + `devDependencies` and classify:
35
75
 
36
- | Found | Framework | Reference doc | CLI auto-wires entry? |
37
- |---|---|---|---|
38
- | `vite` + `react` + `@vitejs/plugin-react` | `vite-react` | `consumer-install-vite.md` | ✅ yes |
39
- | `next` | `nextjs` (App or Pages Router — sniff `app/` vs `pages/`) | `consumer-install-next.md` | ❌ paste snippet |
40
- | `nuxt` | `nuxt` | `consumer-install-nuxt.md` | ❌ paste snippet |
41
- | `astro` | `astro` | `consumer-install-astro.md` | ❌ paste snippet |
42
- | `@remix-run/react` | `remix` | `consumer-install-remix.md` | ❌ paste snippet |
43
- | `vue` ≥ 3 (no `nuxt`) | `vue` | `consumer-install-vue.md` | ❌ paste snippet |
44
- | `@sveltejs/kit` | `sveltekit` | `consumer-install-svelte.md` | ❌ paste snippet |
45
- | none of the above | `plain` / `unknown` | `consumer-install-plain.md` | ❌ CDN snippet |
76
+ | Found | Framework | Reference doc | CLI auto-wires entry? |
77
+ | ----------------------------------------- | --------------------------------------------------------- | ---------------------------- | --------------------- |
78
+ | `vite` + `react` + `@vitejs/plugin-react` | `vite-react` | `consumer-install-vite.md` | ✅ yes |
79
+ | `next` | `nextjs` (App or Pages Router — sniff `app/` vs `pages/`) | `consumer-install-next.md` | ❌ paste snippet |
80
+ | `nuxt` | `nuxt` | `consumer-install-nuxt.md` | ❌ paste snippet |
81
+ | `astro` | `astro` | `consumer-install-astro.md` | ❌ paste snippet |
82
+ | `@remix-run/react` | `remix` | `consumer-install-remix.md` | ❌ paste snippet |
83
+ | `vue` ≥ 3 (no `nuxt`) | `vue` | `consumer-install-vue.md` | ❌ paste snippet |
84
+ | `@sveltejs/kit` | `sveltekit` | `consumer-install-svelte.md` | ❌ paste snippet |
85
+ | none of the above | `plain` / `unknown` | `consumer-install-plain.md` | ❌ CDN snippet |
46
86
 
47
- Order matters: check for `nuxt` *before* generic `vue`, since Nuxt apps have both. Same for `astro` vs the underlying renderer (Astro apps can also have React/Vue/Svelte).
87
+ Order matters: check for `nuxt` _before_ generic `vue`, since Nuxt apps have both. Same for `astro` vs the underlying renderer (Astro apps can also have React/Vue/Svelte).
48
88
 
49
89
  Tell the user which framework you detected. Confirm with `AskUserQuestion` (especially if Next.js, since App vs Pages Router branches differently).
50
90
 
@@ -62,12 +102,14 @@ npx @mhosaic/feedback-cli@latest init \
62
102
  ```
63
103
 
64
104
  This will:
105
+
65
106
  - `npm install @mhosaic/feedback` (using the project's package manager)
66
107
  - Write `.env.local` with `VITE_FEEDBACK_API_KEY=…` + `VITE_FEEDBACK_ENDPOINT=…` (Vite-flavored names; rename per framework if Next/Vue/etc. — see framework references)
67
108
  - Ensure `.env.local` is in `.gitignore`
68
109
  - If `vite-react`: auto-wrap `src/main.tsx` with `<FeedbackProvider>`
69
110
 
70
111
  Surface the CLI's output verbatim. If install fails, stop and read the error message; common causes:
112
+
71
113
  - No internet
72
114
  - Lockfile conflict (offer to delete `pnpm-lock.yaml` / `yarn.lock` / `package-lock.json` and retry)
73
115
  - Wrong directory (no `package.json`)
@@ -86,6 +128,7 @@ Open the per-framework reference document and follow it verbatim:
86
128
  - Plain HTML / CDN: `consumer-install-plain.md`
87
129
 
88
130
  Each reference doc gives:
131
+
89
132
  1. The env-var prefix (e.g. `NEXT_PUBLIC_FEEDBACK_API_KEY` for Next.js — the CLI's default `VITE_FEEDBACK_*` needs renaming)
90
133
  2. The entry-point snippet to paste (with import + provider/init call)
91
134
  3. Where to put it (exact file path)
@@ -103,31 +146,32 @@ If you can't detect the auth provider from `package.json`, ask the user explicit
103
146
 
104
147
  ---
105
148
 
106
- ## Step 7 — (Optional but recommended) Wire the auto-capture modules
149
+ ## Step 7 — Auto-capture modules (error-tracking is on by default)
150
+
151
+ **Error-tracking ships on by default.** `<FeedbackProvider>` arms it automatically (the `errorTracking` prop defaults to `true`), and every non-React snippet in this skill wraps `withErrorTracking` by default. It auto-files synthetic reports for `window.onerror` + `unhandledrejection` — this is the telemetry that feeds the platform's frontend-error Issue scan, so **leave it on** unless the consumer has a specific reason not to. There's nothing to wire for the default case.
152
+
153
+ Two heavier modules stay opt-in (more overhead / more sensitive data):
107
154
 
108
155
  `AskUserQuestion`:
109
- - question: `Wire the auto-capture modules?`
156
+
157
+ - question: `Add the heavier auto-capture modules? (error-tracking is already on)`
110
158
  - options:
111
- - `Yesall three (error-tracking, web-vitals, replay)`, description: `Captures runtime errors, Core Web Vitals, and session replay. ~30 KiB gzip overhead.`
112
- - `Yes — error-tracking only`, description: `Smallest add. Auto-files synthetic reports for window.onerror + unhandledrejection.`
113
- - `No keep it minimal`, description: `Just the manual-submit FAB.`
159
+ - `No — error-tracking only (recommended)`, description: `Keep the default. Smallest footprint.`
160
+ - `Add web-vitals`, description: `Core Web Vitals (LCP/INP/CLS) as telemetry. Small.`
161
+ - `Add web-vitals + replay`, description: `Also session replay (rrweb), ~30 KiB gzip — captures DOM, confirm the privacy posture first.`
114
162
 
115
- For each `Yes`, modify the consumer's widget init to chain the wrappers. The pattern:
163
+ To add the opt-in modules, chain their wrappers around the instance (error-tracking innermost so it arms first):
116
164
 
117
165
  ```typescript
118
- import { createFeedback } from '@mhosaic/feedback'
166
+ import { createFeedback } from '@mhosaic/feedback/loader'
119
167
  import { withErrorTracking } from '@mhosaic/feedback/error-tracking'
120
168
  import { withReplay } from '@mhosaic/feedback/replay'
121
169
  import { withWebVitals } from '@mhosaic/feedback/webvitals'
122
170
 
123
- const fb = withErrorTracking(
124
- withReplay(
125
- withWebVitals(createFeedback({ apiKey, endpoint }))
126
- )
127
- )
171
+ const fb = withReplay(withWebVitals(withErrorTracking(createFeedback({ apiKey, endpoint }))))
128
172
  ```
129
173
 
130
- The CLI's auto-wrap uses `<FeedbackProvider>` (a React-specific helper) for non-Vite-React frameworks the snippet above is the direct equivalent. Match the framework's import style (CommonJS vs ESM, top-level await vs inside an effect).
174
+ **Opting out of error-tracking** (collect-on-consent posture): pass `<FeedbackProvider errorTracking={false}>`, or on the `createFeedback` path simply omit the `withErrorTracking` wrap. Note it POSTs error messages + stack traces — fine for the vast majority of apps, but flag it if the consumer's error strings can carry end-user PII.
131
175
 
132
176
  ---
133
177
 
@@ -140,12 +184,14 @@ npx @mhosaic/feedback-cli@latest verify \
140
184
  ```
141
185
 
142
186
  This is a lightweight check; it confirms:
187
+
143
188
  - `.env.local` has the right shape
144
189
  - The endpoint resolves and responds
145
190
  - CORS allows the consumer's origin
146
191
  - The public key is accepted (POSTs a `[mhosaic-feedback verify]` synthetic report)
147
192
 
148
193
  If anything is red, the verify command prints a hint. Common hints:
194
+
149
195
  - `add this origin to the project's allowed_origins (admin SPA: Edit project)` → operator needs to add the dev/prod origin.
150
196
  - `key invalid or revoked` → operator needs to mint a new key.
151
197
 
@@ -174,10 +220,11 @@ Summarize for the user:
174
220
  - Widget installed ✓
175
221
  - FAB renders ✓
176
222
  - Smoke-test report received ✓
177
- - Optional modules wired: [error-tracking / replay / web-vitals / none]
223
+ - Error-tracking: on by default ✓ (or `errorTracking={false}` if the consumer opted out) · extra modules wired: [web-vitals / replay / none]
178
224
  - Next: deploy to staging, add the staging URL to `allowed_origins` (operator action — DM them the URL)
179
225
 
180
226
  Encourage them to:
227
+
181
228
  - Commit the change (sans `.env.local`)
182
229
  - Tell the operator the smoke test passed
183
230
  - Delete the smoke-test reports from `/reports` in admin once it lands
@@ -14,7 +14,7 @@ The widget's FAB is hidden until `fb.identify()` is called with a non-empty `id`
14
14
 
15
15
  import { useEffect } from 'react'
16
16
  import { useAuth0 } from '@auth0/auth0-react'
17
- import { useFeedback } from '@mhosaic/feedback/react'
17
+ import { useFeedback } from '@mhosaic/feedback/loader/react'
18
18
 
19
19
  export function FeedbackIdentity() {
20
20
  const { user, isAuthenticated, isLoading } = useAuth0()
@@ -40,7 +40,7 @@ Mount `<FeedbackIdentity />` once inside both `<Auth0Provider>` and `<FeedbackPr
40
40
  ```tsx
41
41
  import { useEffect } from 'react'
42
42
  import { useUser } from '@clerk/clerk-react'
43
- import { useFeedback } from '@mhosaic/feedback/react'
43
+ import { useFeedback } from '@mhosaic/feedback/loader/react'
44
44
 
45
45
  export function FeedbackIdentity() {
46
46
  const { isLoaded, isSignedIn, user } = useUser()
@@ -118,7 +118,7 @@ Place after `initializeApp()` and `createFeedback()`. The callback fires immedia
118
118
 
119
119
  import { useEffect } from 'react'
120
120
  import { useSession } from 'next-auth/react'
121
- import { useFeedback } from '@mhosaic/feedback/react'
121
+ import { useFeedback } from '@mhosaic/feedback/loader/react'
122
122
 
123
123
  export function FeedbackIdentity() {
124
124
  const { data: session, status } = useSession()