@myapihq/cli 2.18.0 → 2.19.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/dist/commands/feedback-capture.test.d.ts +1 -0
- package/dist/commands/feedback-capture.test.js +49 -0
- package/dist/commands/feedback.d.ts +8 -0
- package/dist/commands/feedback.js +101 -7
- package/dist/errors.js +6 -0
- package/dist/skills/my-feedback-api/SKILL.md +98 -81
- package/package.json +2 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Capture flags send only what the operator named.
|
|
2
|
+
//
|
|
3
|
+
// The rule this file defends: an absent `capture` sub-field means "leave it
|
|
4
|
+
// alone", never "off". ImmoPilot's screens carry tenant names, IBANs and dates
|
|
5
|
+
// of birth; the widget photographs the viewport and reads the text of whatever
|
|
6
|
+
// the reporter points at. If `--no-screenshot` also sent `text: true` because
|
|
7
|
+
// the CLI helpfully filled in the object, it would turn one deliberate
|
|
8
|
+
// restriction into a different, undeclared permission — on exactly those pages.
|
|
9
|
+
//
|
|
10
|
+
// Both fields default to true server-side, so this is also what keeps every
|
|
11
|
+
// widget embedded before capture existed behaving as it always did.
|
|
12
|
+
import { describe, it, expect } from 'vitest';
|
|
13
|
+
import { _captureFrom } from './feedback.js';
|
|
14
|
+
const f = (o) => o;
|
|
15
|
+
describe('_captureFrom', () => {
|
|
16
|
+
it('is undefined when the operator said nothing', () => {
|
|
17
|
+
// Not `{}` — an empty object would still put `capture` in the PATCH body,
|
|
18
|
+
// and "Nothing to change" would stop being reachable.
|
|
19
|
+
expect(_captureFrom(f({}))).toBeUndefined();
|
|
20
|
+
expect(_captureFrom(f({ routes: '/a' }))).toBeUndefined();
|
|
21
|
+
});
|
|
22
|
+
it('sends only the field that was named', () => {
|
|
23
|
+
expect(_captureFrom(f({ 'no-screenshot': true }))).toEqual({ screenshot: false });
|
|
24
|
+
expect(_captureFrom(f({ 'no-text': true }))).toEqual({ text: false });
|
|
25
|
+
expect(_captureFrom(f({ screenshot: true }))).toEqual({ screenshot: true });
|
|
26
|
+
expect(_captureFrom(f({ text: true }))).toEqual({ text: true });
|
|
27
|
+
});
|
|
28
|
+
it('never infers the field it was not told about', () => {
|
|
29
|
+
// The whole point. Turning screenshots off must not carry an opinion about
|
|
30
|
+
// text, in either direction.
|
|
31
|
+
const only = _captureFrom(f({ 'no-screenshot': true }));
|
|
32
|
+
expect(only).not.toHaveProperty('text');
|
|
33
|
+
expect(Object.keys(only)).toEqual(['screenshot']);
|
|
34
|
+
});
|
|
35
|
+
it('carries both when both were named', () => {
|
|
36
|
+
expect(_captureFrom(f({ 'no-screenshot': true, 'no-text': true })))
|
|
37
|
+
.toEqual({ screenshot: false, text: false });
|
|
38
|
+
expect(_captureFrom(f({ screenshot: true, 'no-text': true })))
|
|
39
|
+
.toEqual({ screenshot: true, text: false });
|
|
40
|
+
});
|
|
41
|
+
it('lets the negative win when both spellings are passed', () => {
|
|
42
|
+
// `--screenshot --no-screenshot` is contradictory input. Resolving it to
|
|
43
|
+
// the restrictive answer is the safe direction: the cost of wrongly not
|
|
44
|
+
// capturing is a less useful bug report, the cost of wrongly capturing is
|
|
45
|
+
// a customer's records in a screenshot.
|
|
46
|
+
expect(_captureFrom(f({ screenshot: true, 'no-screenshot': true }))).toEqual({ screenshot: false });
|
|
47
|
+
expect(_captureFrom(f({ text: true, 'no-text': true }))).toEqual({ text: false });
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -1,8 +1,16 @@
|
|
|
1
|
+
import { feedback as sdkFeedback } from '@myapihq/sdk';
|
|
1
2
|
import type { FlagSchema } from '../flags.js';
|
|
2
3
|
import { type Flags } from '../helpers.js';
|
|
3
4
|
import type { Exposes } from '../exposes.js';
|
|
4
5
|
export declare const EXPOSES: Exposes;
|
|
5
6
|
export declare const SCHEMA: FlagSchema;
|
|
7
|
+
/** Reads the paired --x / --no-x flags into a capture patch.
|
|
8
|
+
*
|
|
9
|
+
* Only the fields the operator actually named end up in the object. Sending a
|
|
10
|
+
* full `capture` would turn "stop taking screenshots" into "stop taking
|
|
11
|
+
* screenshots and start capturing text", silently changing a setting they did
|
|
12
|
+
* not mention — on a widget that may be pointed at tenant records. */
|
|
13
|
+
export declare function _captureFrom(flags: Flags): sdkFeedback.WidgetCapture | undefined;
|
|
6
14
|
export declare function list(flags: Flags): Promise<void>;
|
|
7
15
|
export declare function create(bodyArg: string | undefined, flags: Flags): Promise<void>;
|
|
8
16
|
export declare function resolve(id: string, flags: Flags): Promise<void>;
|
|
@@ -36,7 +36,41 @@ export const SCHEMA = {
|
|
|
36
36
|
'page-url': 'string',
|
|
37
37
|
route: 'string',
|
|
38
38
|
origins: 'string',
|
|
39
|
+
// Capture controls. Paired --x / --no-x rather than --x=true|false so that
|
|
40
|
+
// "unset" stays distinguishable from "set to false": the API treats an absent
|
|
41
|
+
// key as unchanged, and the CLI must be able to express that.
|
|
42
|
+
screenshot: 'boolean',
|
|
43
|
+
'no-screenshot': 'boolean',
|
|
44
|
+
text: 'boolean',
|
|
45
|
+
'no-text': 'boolean',
|
|
39
46
|
};
|
|
47
|
+
/** Reads the paired --x / --no-x flags into a capture patch.
|
|
48
|
+
*
|
|
49
|
+
* Only the fields the operator actually named end up in the object. Sending a
|
|
50
|
+
* full `capture` would turn "stop taking screenshots" into "stop taking
|
|
51
|
+
* screenshots and start capturing text", silently changing a setting they did
|
|
52
|
+
* not mention — on a widget that may be pointed at tenant records. */
|
|
53
|
+
export function _captureFrom(flags) {
|
|
54
|
+
const cap = {};
|
|
55
|
+
if (flags['no-screenshot'])
|
|
56
|
+
cap.screenshot = false;
|
|
57
|
+
else if (flags.screenshot)
|
|
58
|
+
cap.screenshot = true;
|
|
59
|
+
if (flags['no-text'])
|
|
60
|
+
cap.text = false;
|
|
61
|
+
else if (flags.text)
|
|
62
|
+
cap.text = true;
|
|
63
|
+
return Object.keys(cap).length ? cap : undefined;
|
|
64
|
+
}
|
|
65
|
+
/** "on" / "off" for a resolved capture field. The server returns both fields
|
|
66
|
+
* resolved, so an undefined here means an older backend that did not answer —
|
|
67
|
+
* say so rather than printing "off" for something that is on. */
|
|
68
|
+
function captureLine(c) {
|
|
69
|
+
if (!c)
|
|
70
|
+
return '(not reported by this backend)';
|
|
71
|
+
const s = (v) => v === undefined ? '?' : v ? 'on' : 'off';
|
|
72
|
+
return `screenshot ${s(c.screenshot)}, text ${s(c.text)}`;
|
|
73
|
+
}
|
|
40
74
|
// The platform's enum, checked against the schema rather than invented.
|
|
41
75
|
const KINDS = ['bug', 'issue', 'suggestion'];
|
|
42
76
|
// A selector says `div#install > button.save`. The context says the button read
|
|
@@ -96,6 +130,15 @@ export async function list(flags) {
|
|
|
96
130
|
const pointing = describeTarget(i);
|
|
97
131
|
if (pointing)
|
|
98
132
|
info(` pointing at ${pointing}`);
|
|
133
|
+
// Whoever the host app named via window.__myapiFeedback.identify(). Opaque
|
|
134
|
+
// to us — their id for their user, never resolved against anything.
|
|
135
|
+
//
|
|
136
|
+
// Printed only when set: an anonymous visitor is still the normal case, and
|
|
137
|
+
// a blank `from` reads as a failure to capture rather than nobody to name.
|
|
138
|
+
// Every item filed before 2026-08-19 carries '' — the column existed and
|
|
139
|
+
// nothing ever wrote it — so most history will show no line here.
|
|
140
|
+
if (i.end_user_ref)
|
|
141
|
+
info(` from ${i.end_user_ref}`);
|
|
99
142
|
const trace = Array.isArray(i.trace) ? i.trace : [];
|
|
100
143
|
if (trace.length) {
|
|
101
144
|
// Errors and failed requests are the highest-signal part — for most
|
|
@@ -197,13 +240,14 @@ export async function widget(sub, arg, flags) {
|
|
|
197
240
|
const origins = typeof flags.origins === 'string'
|
|
198
241
|
? flags.origins.split(',').map(s => s.trim()).filter(Boolean)
|
|
199
242
|
: undefined;
|
|
200
|
-
const w = await sdkFeedback.createWidget(config.api_key, orgId, arg, origins);
|
|
243
|
+
const w = await sdkFeedback.createWidget(config.api_key, orgId, arg, origins, _captureFrom(flags));
|
|
201
244
|
if (flags.json) {
|
|
202
245
|
printJson(w);
|
|
203
246
|
return;
|
|
204
247
|
}
|
|
205
248
|
success(`Widget created: ${w.id}`);
|
|
206
249
|
info(`Key: ${w.key}`);
|
|
250
|
+
info(`Capture: ${captureLine(w.capture)}`);
|
|
207
251
|
info('');
|
|
208
252
|
// Saying this plainly matters: a value that looks like a credential and is
|
|
209
253
|
// not one gets treated as a secret, and then nobody puts it in the page.
|
|
@@ -239,12 +283,16 @@ export async function widget(sub, arg, flags) {
|
|
|
239
283
|
.filter(Boolean).join(' · ')}`);
|
|
240
284
|
}
|
|
241
285
|
info(` origins: ${w.allowed_origins?.length ? w.allowed_origins.join(', ') : '(any — anyone can post with this key)'}`);
|
|
286
|
+
// Listed for the same reason position and label are: the only other way
|
|
287
|
+
// to read it back was to fetch widget.js and read the JavaScript, which
|
|
288
|
+
// ImmoPilot reported once already.
|
|
289
|
+
info(` capture: ${captureLine(w.capture)}`);
|
|
242
290
|
info('');
|
|
243
291
|
}
|
|
244
292
|
return;
|
|
245
293
|
}
|
|
246
294
|
if (sub === 'update') {
|
|
247
|
-
requireArg(arg, 'id', 'myapi feedback widget update <id> [--routes /a,/b] [--origins a.com] [--accent #2563eb] [--position bottom-right] [--label "Feedback"]');
|
|
295
|
+
requireArg(arg, 'id', 'myapi feedback widget update <id> [--routes /a,/b] [--origins a.com] [--accent #2563eb] [--position bottom-right] [--label "Feedback"] [--no-screenshot] [--no-text]');
|
|
248
296
|
const patch = {};
|
|
249
297
|
const csv = (v) => typeof v === 'string' ? v.split(',').map(s => s.trim()).filter(Boolean) : undefined;
|
|
250
298
|
if (flags.routes !== undefined)
|
|
@@ -260,8 +308,36 @@ export async function widget(sub, arg, flags) {
|
|
|
260
308
|
}
|
|
261
309
|
if (Object.keys(theme).length)
|
|
262
310
|
patch.theme = theme;
|
|
311
|
+
// Read-modify-write, because the backend REPLACES `capture` rather than
|
|
312
|
+
// merging it. Verified against production 2026-08-20: set
|
|
313
|
+
// {screenshot:false, text:false}, PATCH {text:true}, and screenshot comes
|
|
314
|
+
// back TRUE. Only the capture object behaves this way — a name-only PATCH
|
|
315
|
+
// leaves it untouched — so the request as a whole merges and this field
|
|
316
|
+
// does not.
|
|
317
|
+
//
|
|
318
|
+
// --help and the skill both promise "omitted flags are left alone". Sending
|
|
319
|
+
// the partial object would make `--no-text` silently switch screenshots
|
|
320
|
+
// back ON for a widget that had them off: the exact harm this feature
|
|
321
|
+
// exists to prevent, on the pages it was built for.
|
|
322
|
+
//
|
|
323
|
+
// Remove once the backend merges (cross-repo prompt, 2026-08-20): then
|
|
324
|
+
// `patch.capture = capture` stands alone and the extra read disappears.
|
|
325
|
+
const capture = _captureFrom(flags);
|
|
326
|
+
if (capture) {
|
|
327
|
+
patch.capture = capture;
|
|
328
|
+
try {
|
|
329
|
+
const current = (await sdkFeedback.listWidgets(config.api_key, orgId)).find(x => x.id === arg);
|
|
330
|
+
if (current?.capture)
|
|
331
|
+
patch.capture = { ...current.capture, ...capture };
|
|
332
|
+
}
|
|
333
|
+
catch {
|
|
334
|
+
// A failed read must not block the change that was asked for. The
|
|
335
|
+
// operator still gets what they typed, and the resolved state printed
|
|
336
|
+
// after the call is the truth either way.
|
|
337
|
+
}
|
|
338
|
+
}
|
|
263
339
|
if (Object.keys(patch).length === 0) {
|
|
264
|
-
error('Nothing to change. Pass at least one of --routes, --origins, --name, --accent, --position, --label.');
|
|
340
|
+
error('Nothing to change. Pass at least one of --routes, --origins, --name, --accent, --position, --label, --screenshot/--no-screenshot, --text/--no-text.');
|
|
265
341
|
}
|
|
266
342
|
const w = await sdkFeedback.updateWidget(config.api_key, orgId, arg, patch);
|
|
267
343
|
if (flags.json) {
|
|
@@ -269,9 +345,13 @@ export async function widget(sub, arg, flags) {
|
|
|
269
345
|
return;
|
|
270
346
|
}
|
|
271
347
|
success(`Widget ${arg} updated`);
|
|
348
|
+
// Resolved by the server: both fields, whatever was sent. Print it so the
|
|
349
|
+
// operator sees what the widget will DO, not what they happened to type.
|
|
350
|
+
if (w.capture)
|
|
351
|
+
info(` capture: ${captureLine(w.capture)}`);
|
|
272
352
|
// The key is unchanged, which is the reason to use this instead of
|
|
273
353
|
// revoke + create — the customer's page does not need editing.
|
|
274
|
-
info('Same key, so your page needs no change. Route changes reach browsers within ~5 minutes.');
|
|
354
|
+
info('Same key, so your page needs no change. Route and capture changes reach browsers within ~5 minutes, with no redeploy of your site.');
|
|
275
355
|
return;
|
|
276
356
|
}
|
|
277
357
|
if (sub === 'revoke') {
|
|
@@ -282,7 +362,7 @@ export async function widget(sub, arg, flags) {
|
|
|
282
362
|
info('Feedback already collected through it is kept.');
|
|
283
363
|
return;
|
|
284
364
|
}
|
|
285
|
-
error('Usage: myapi feedback widget create <name> [--origins <list>]\n myapi feedback widget list\n myapi feedback widget update <id> [--routes <list>] [--origins <list>]\n myapi feedback widget revoke <id>');
|
|
365
|
+
error('Usage: myapi feedback widget create <name> [--origins <list>] [--no-screenshot] [--no-text]\n myapi feedback widget list\n myapi feedback widget update <id> [--routes <list>] [--origins <list>] [--no-screenshot] [--no-text]\n myapi feedback widget revoke <id>');
|
|
286
366
|
}
|
|
287
367
|
export const SUBCOMMAND_USAGE = {
|
|
288
368
|
'delete': `myapi feedback delete <id> [--yes] [--org <id>]
|
|
@@ -300,10 +380,24 @@ Newest first. \`total\` is the number of matches, not the page size.`,
|
|
|
300
380
|
'resolve': 'myapi feedback resolve <id> [--org <id>]',
|
|
301
381
|
'widget': `myapi feedback widget list
|
|
302
382
|
myapi feedback widget create <name> [--origins a.com,b.com] [--org <id>]
|
|
383
|
+
myapi feedback widget update <id> [--routes <list>] [--no-screenshot] [--no-text] [--org <id>]
|
|
303
384
|
myapi feedback widget revoke <id> [--yes] [--org <id>]
|
|
304
385
|
|
|
305
386
|
The key a widget mints is PUBLIC — it ships in page source and authenticates
|
|
306
|
-
nobody. --origins stops another site posting through it
|
|
387
|
+
nobody. --origins stops another site posting through it.
|
|
388
|
+
|
|
389
|
+
What a report may collect (both on by default):
|
|
390
|
+
--no-screenshot Stop photographing the viewport.
|
|
391
|
+
--no-text Keep the SHAPE of the element pointed at — tag, role, id,
|
|
392
|
+
data-feedback-id — and drop the words inside it. On a table
|
|
393
|
+
row that is "row, id row-8" instead of the customer's name.
|
|
394
|
+
Pass --screenshot / --text to turn one back on. Flags you omit are left alone,
|
|
395
|
+
so turning one off never silently changes the other. Changes reach live
|
|
396
|
+
browsers within ~5 minutes, with no redeploy of your site.
|
|
397
|
+
|
|
398
|
+
To exempt one panel rather than the whole widget, mark it data-feedback-ignore
|
|
399
|
+
in your page: it is skipped in the element context AND blacked out in the
|
|
400
|
+
screenshot.`,
|
|
307
401
|
};
|
|
308
402
|
export async function run(subcommand, args, flags) {
|
|
309
403
|
if (!subcommand || (flags.help && !subcommand)) {
|
|
@@ -320,7 +414,7 @@ Subcommands:
|
|
|
320
414
|
widget create <name> Mint a PUBLIC widget key for a site (--origins to restrict)
|
|
321
415
|
widget list List widgets with the script tag to paste
|
|
322
416
|
widget revoke <id> Revoke a widget key; collected feedback is kept
|
|
323
|
-
widget update <id> Change routes/origins/theme, keeping the same key
|
|
417
|
+
widget update <id> Change routes/origins/theme/capture, keeping the same key
|
|
324
418
|
|
|
325
419
|
All commands accept --org <id> (or set default: myapi config set-org <id>).`);
|
|
326
420
|
return;
|
package/dist/errors.js
CHANGED
|
@@ -26,6 +26,12 @@ export const ERROR_MESSAGES = {
|
|
|
26
26
|
invalid_org: 'Invalid organization.',
|
|
27
27
|
FORBIDDEN: 'You do not have permission to perform this action.',
|
|
28
28
|
RATE_LIMITED: 'Too many requests. Please wait a moment and try again.',
|
|
29
|
+
// The backend refuses an unknown key or a non-boolean rather than ignoring it
|
|
30
|
+
// — `{"screenshots": false}` (plural) is a 422, not a no-op. That is
|
|
31
|
+
// deliberate: a capture setting accepted and ignored reads as "screenshots
|
|
32
|
+
// are off" while screenshots keep being taken, on pages that may carry
|
|
33
|
+
// customer records.
|
|
34
|
+
INVALID_CAPTURE: 'capture accepts only `screenshot` and `text`, both booleans. Check the spelling — an unknown key is refused, not ignored.',
|
|
29
35
|
BODY_TOO_LARGE: 'The request is over 64 KB — usually an oversized feedback `trace`. Nothing was saved. This used to surface as INVALID_JSON, which sent people looking for a syntax error they did not have.',
|
|
30
36
|
INSUFFICIENT_BALANCE: 'Insufficient balance. Run: myapi billing topup <amount>',
|
|
31
37
|
INVALID_AMOUNT: 'Amount out of range. Maximum single top-up is $100. Run: myapi billing topup <amount>',
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: my-feedback-api
|
|
3
|
-
version: 1.
|
|
3
|
+
version: 1.6.0
|
|
4
4
|
description: >
|
|
5
5
|
Collect feedback from the people using what you built. A public widget key lets a page submit without a credential; you list, filter and resolve the results. Kind is chosen by the person reporting, not inferred from their wording.
|
|
6
6
|
triggers: [feedback, bug report, user feedback, feature request, widget, support, complaints, praise]
|
|
7
|
-
checksum: sha256-
|
|
7
|
+
checksum: sha256-f8be1cee17e2dc5e1dcf2310cf18d275982d54460af26a0b642fc46bd8c4691e
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# MyFeedbackAPI
|
|
@@ -18,111 +18,129 @@ Two halves: a **widget key** a page embeds, and the **items** it produces.
|
|
|
18
18
|
|
|
19
19
|
### The widget key is public, and that is the point
|
|
20
20
|
|
|
21
|
-
`
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
`widget create <name>` mints a key that ships in your page source. It is **not a
|
|
22
|
+
secret** — it names your org so a visitor can submit without signing in, and
|
|
23
|
+
authenticates nobody.
|
|
24
24
|
|
|
25
|
-
Treating it as a credential is the mistake
|
|
26
|
-
|
|
27
|
-
cannot post through it:
|
|
25
|
+
Treating it as a credential is the mistake: people hide it and the widget cannot
|
|
26
|
+
work. It needs `--origins`, so another site cannot post through it:
|
|
28
27
|
|
|
29
28
|
```bash
|
|
30
29
|
myapi feedback widget create site --origins app.example.com,example.com
|
|
31
30
|
```
|
|
32
31
|
|
|
33
|
-
Revoke with `myapi feedback widget revoke <id
|
|
34
|
-
feedback
|
|
32
|
+
Revoke with `myapi feedback widget revoke <id>`: submissions stop immediately,
|
|
33
|
+
collected feedback is kept.
|
|
35
34
|
|
|
36
35
|
### Embedding it: one script tag
|
|
37
36
|
|
|
38
|
-
The platform hosts the widget, compiled per key with
|
|
39
|
-
|
|
37
|
+
The platform hosts the widget, compiled per key with its routes and theme baked
|
|
38
|
+
in. The intended way in — you build no UI:
|
|
40
39
|
|
|
41
40
|
```html
|
|
42
41
|
<script src="https://api.myapihq.com/feedback/in/<widget_key>/widget.js" async></script>
|
|
43
42
|
```
|
|
44
43
|
|
|
45
|
-
It
|
|
46
|
-
|
|
47
|
-
the change reaches browsers within ~5 minutes with no redeploy of your site.
|
|
44
|
+
It shows only on the widget's routes — set them with `widget update <id>
|
|
45
|
+
--routes /app,/app/**`; live in ~5 min, no redeploy.
|
|
48
46
|
|
|
49
|
-
The person points at an element or drags a region
|
|
47
|
+
The person points at an element or drags a region; it sends:
|
|
50
48
|
|
|
51
49
|
- `kind`, `body`, `page_url`, `route`, `viewport`
|
|
52
|
-
- `target_selector` and `target_region` `{x,y,w,h}` —
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
- `target_context` — what the element *was*: its text, role, the heading above it
|
|
50
|
+
- `target_selector` and `target_region` `{x,y,w,h}` — on **every** report: the
|
|
51
|
+
element's bounding box for a click, the dragged box for a drag
|
|
52
|
+
- `target_context` — what the element *was*: text, role, the heading above it
|
|
56
53
|
- `trace` — the last events before the report
|
|
57
54
|
|
|
58
|
-
A wrong key
|
|
59
|
-
|
|
60
|
-
in your app.
|
|
55
|
+
A wrong key cannot break the page: the response is `application/javascript`
|
|
56
|
+
with a comment naming the fix, so a typo throws no syntax error.
|
|
61
57
|
|
|
62
|
-
**Screenshots are automatic — you
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
deliberate: an image that fails to render or upload never costs the report.
|
|
58
|
+
**Screenshots are automatic — you build no capture step.** The widget attaches
|
|
59
|
+
the picture as a second call after the report is saved, so an image that fails
|
|
60
|
+
to render never costs the report.
|
|
66
61
|
|
|
67
|
-
The image is **
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
JPEG or WebP up to 3 MB; over that the feedback is kept without the picture.
|
|
62
|
+
The image is **private in your org's assets**; listing returns a **signed link
|
|
63
|
+
good for ~an hour** (`screenshot_url`) — re-list rather than persisting it.
|
|
64
|
+
PNG/JPEG/WebP up to 3 MB; over that the report is kept without it.
|
|
71
65
|
|
|
72
66
|
### Submitting by hand (only if you cannot use the script)
|
|
73
67
|
|
|
74
|
-
`POST https://api.myapihq.com/feedback/in/<widget_key>` — no auth, no SDK
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
```js
|
|
78
|
-
await fetch(`https://api.myapihq.com/feedback/in/${WIDGET_KEY}`, {
|
|
79
|
-
method: 'POST',
|
|
80
|
-
headers: { 'Content-Type': 'application/json' },
|
|
81
|
-
body: JSON.stringify({ kind: 'bug', body: text, page_url: location.href }),
|
|
82
|
-
});
|
|
83
|
-
```
|
|
68
|
+
`POST https://api.myapihq.com/feedback/in/<widget_key>` — no auth, no SDK, same
|
|
69
|
+
fields the widget sends (`kind`, `body`, `page_url`, …).
|
|
84
70
|
|
|
85
71
|
Errors: `WIDGET_NOT_FOUND` (a revoked key reads like an invented one, so keys
|
|
86
72
|
cannot be probed), `ORIGIN_NOT_ALLOWED`, `RATE_LIMITED`, `INVALID_KIND`,
|
|
87
|
-
`BODY_REQUIRED`, `BODY_TOO_LONG` (
|
|
88
|
-
`
|
|
89
|
-
`trace`). Nothing is saved in either case. The key only ever writes.
|
|
73
|
+
`BODY_REQUIRED`, `BODY_TOO_LONG` (>8000 chars), `BODY_TOO_LARGE` (>64 KB, usually
|
|
74
|
+
an oversized `trace`). Nothing is saved; the key only writes.
|
|
90
75
|
|
|
91
76
|
### Kind is a claim, not a guess
|
|
92
77
|
|
|
93
|
-
`--kind` is one of `bug`, `issue`, `suggestion`, and it is **what the
|
|
94
|
-
|
|
95
|
-
they believe the product is broken — which is a different and more urgent
|
|
78
|
+
`--kind` is one of `bug`, `issue`, `suggestion`, and it is **what the reporter
|
|
79
|
+
says it is**. A `bug` means they believe the product is broken — a more urgent
|
|
96
80
|
signal than a wish for something new. Do not re-classify from the wording.
|
|
97
81
|
|
|
98
|
-
Classification and duplicate-grouping
|
|
99
|
-
|
|
100
|
-
|
|
82
|
+
Classification and duplicate-grouping are designed but not built, so treat
|
|
83
|
+
`kind` as the person's own label, not a processed signal.
|
|
84
|
+
|
|
85
|
+
### Controlling what a report contains
|
|
86
|
+
|
|
87
|
+
A report carries a screenshot of the viewport plus the text of the element
|
|
88
|
+
pointed at. On a page of customer records, that is the page. Both on by default:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
myapi feedback widget update <id> --no-screenshot # stop photographing
|
|
92
|
+
myapi feedback widget update <id> --no-text # keep SHAPE, drop words
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
`--no-text` keeps tag, role, id and `data-feedback-id`: on a table row, "row, id
|
|
96
|
+
`row-8`" not the tenant's name. `--screenshot` / `--text` turn one back on.
|
|
97
|
+
Omitted flags are left alone, so one off never changes the other. Live in ~5 min.
|
|
98
|
+
|
|
99
|
+
**In your page**, two levers only you control:
|
|
100
|
+
|
|
101
|
+
```js
|
|
102
|
+
window.__myapiFeedback.identify("your-user-id"); // when you learn who it is
|
|
103
|
+
window.__myapiFeedback.identify(); // on sign-out
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Callable any time, even long after load. Your id for your user — opaque to us,
|
|
107
|
+
never resolved, posted to a public endpoint: a label, not authentication. Shows
|
|
108
|
+
as `from` in `feedback list`.
|
|
109
|
+
|
|
110
|
+
Mark an element `data-feedback-ignore` to skip it in the context AND black it out
|
|
111
|
+
in the screenshot.
|
|
112
|
+
|
|
113
|
+
| Need | Lever |
|
|
114
|
+
|---|---|
|
|
115
|
+
| No pictures at all | `--no-screenshot` |
|
|
116
|
+
| Pictures, no words | `--no-text` |
|
|
117
|
+
| All but one panel | `data-feedback-ignore` |
|
|
118
|
+
| Different rules per page | two widgets, two route sets |
|
|
119
|
+
|
|
120
|
+
The last row is usually right: capture off on record-bearing routes, on
|
|
121
|
+
elsewhere — not one global switch.
|
|
101
122
|
|
|
102
123
|
### Reading it back
|
|
103
124
|
|
|
104
125
|
Each item carries what was pointed at, how they got there, and a picture.
|
|
105
|
-
`
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
through.
|
|
109
|
-
|
|
110
|
-
Two
|
|
111
|
-
a
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
`myapi feedback resolve <id>` closes an item. An unknown id answers the same
|
|
124
|
-
way as an already-resolved one, so a success is not proof the item existed —
|
|
125
|
-
that is deliberate, so ids cannot be probed across orgs.
|
|
126
|
+
`list` summarises it — the element's own text rather than its CSS selector,
|
|
127
|
+
counts of steps/errors/failed requests, whether a screenshot exists, and `from`
|
|
128
|
+
when the page called `identify()`. `--trace` expands the events; `--json` passes
|
|
129
|
+
everything through.
|
|
130
|
+
|
|
131
|
+
Two not to misread: **`trace[].t` is ms since page load**, not clock time — only
|
|
132
|
+
a delta means anything. And **`target_context.filled` says an input had a value,
|
|
133
|
+
never what**.
|
|
134
|
+
|
|
135
|
+
`delete` removes the screenshot with the report — what matters is somebody
|
|
136
|
+
erasing a report because of what is in the picture.
|
|
137
|
+
|
|
138
|
+
`list` is newest first, filterable by `--kind`/`--status`, paged with `--limit`
|
|
139
|
+
/ `--offset`. **`total` is the match count, not the page size**; `has_more` flags
|
|
140
|
+
truncation — both describe the whole match set.
|
|
141
|
+
|
|
142
|
+
`resolve <id>` closes an item. An unknown id answers like an already-resolved
|
|
143
|
+
one, so success is not proof it existed — deliberate, so ids cannot be probed.
|
|
126
144
|
<!-- llm:end -->
|
|
127
145
|
|
|
128
146
|
## Commands
|
|
@@ -135,7 +153,7 @@ that is deliberate, so ids cannot be probed across orgs.
|
|
|
135
153
|
| `myapi feedback delete <id>` | Erase an item — for spam, or personal details typed into the box |
|
|
136
154
|
| `myapi feedback widget create <name> [--origins a.com,b.com]` | Mint a PUBLIC widget key for a site |
|
|
137
155
|
| `myapi feedback widget list` | List widgets, each with the script tag to paste |
|
|
138
|
-
| `myapi feedback widget update <id>` | Change `--routes`, `--origins`, `--name`,
|
|
156
|
+
| `myapi feedback widget update <id>` | Change `--routes`, `--origins`, `--name`, look (`--accent`/`--position`/`--label`) or capture (`--no-screenshot`/`--no-text`); same key |
|
|
139
157
|
| `myapi feedback widget revoke <id>` | Revoke a key; collected feedback is kept |
|
|
140
158
|
<!-- generated:end -->
|
|
141
159
|
|
|
@@ -149,16 +167,16 @@ myapi feedback widget create marketing --origins example.com,www.example.com
|
|
|
149
167
|
# 2. See your widgets and the exact tag to paste
|
|
150
168
|
myapi feedback widget list
|
|
151
169
|
|
|
152
|
-
# 3. Change where it appears
|
|
170
|
+
# 3. Change where it appears, or what it may collect — same key either way
|
|
153
171
|
myapi feedback widget update <id> --routes /app,/app/**
|
|
172
|
+
myapi feedback widget update <id> --no-screenshot --no-text # record-bearing routes
|
|
154
173
|
|
|
155
|
-
# 4. Read what came back, newest first
|
|
174
|
+
# 4. Read what came back, newest first (`from` shows if the page identify()d)
|
|
156
175
|
myapi feedback list --status open
|
|
157
176
|
myapi feedback list --kind bug --limit 20
|
|
158
177
|
|
|
159
178
|
# 5. Record something yourself (support call, your own testing)
|
|
160
|
-
myapi feedback create "checkout 500s on
|
|
161
|
-
--route /checkout
|
|
179
|
+
myapi feedback create "checkout 500s on retry" --kind bug --route /checkout
|
|
162
180
|
|
|
163
181
|
# 6. Close it — or delete it, which also removes its screenshot
|
|
164
182
|
myapi feedback resolve <id>
|
|
@@ -168,12 +186,11 @@ myapi feedback delete <id> --yes
|
|
|
168
186
|
|
|
169
187
|
## Notes
|
|
170
188
|
|
|
171
|
-
- The widget key is public by design — restrict
|
|
172
|
-
- `resolve` is idempotent and deliberately indistinguishable from an unknown id.
|
|
189
|
+
- The widget key is public by design — restrict with `--origins`, don't hide it.
|
|
173
190
|
- Feedback survives widget revocation.
|
|
174
|
-
- **`resolve` keeps, `delete` erases.** A public inbox collects spam and the
|
|
175
|
-
|
|
176
|
-
|
|
191
|
+
- **`resolve` keeps, `delete` erases.** A public inbox collects spam and the odd
|
|
192
|
+
report with personal details in it; `delete` is how those leave. Both are
|
|
193
|
+
idempotent and answer the same for an unknown id, so ids cannot be probed.
|
|
177
194
|
|
|
178
195
|
## HTTP (from deployed code)
|
|
179
196
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@myapihq/cli",
|
|
3
3
|
"license": "Apache-2.0",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.19.1",
|
|
5
5
|
"description": "MyAPI command-line interface",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"lint:skills:strict": "node scripts/copy-skills.js && node scripts/lint-skills.js --strict"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@myapihq/sdk": "^2.
|
|
49
|
+
"@myapihq/sdk": "^2.19.1"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@types/node": "^25.6.0",
|